From a2bd841f6faabb5ab03ec6832ac82eaff8a11b62 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Wed, 14 Feb 2024 22:01:09 +0500 Subject: [PATCH 01/33] feature: Initial Implementation of `lost-password` api endpoint --- .../org/lowcoder/domain/user/service/UserService.java | 2 ++ .../lowcoder/domain/user/service/UserServiceImpl.java | 10 ++++++++++ .../lowcoder/api/usermanagement/UserApiService.java | 4 ++++ .../lowcoder/api/usermanagement/UserController.java | 9 +++++++++ .../org/lowcoder/api/usermanagement/UserEndpoints.java | 6 ++++++ 5 files changed, 31 insertions(+) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index aebed82ef..497f50f1e 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -50,6 +50,8 @@ public interface UserService { Mono resetPassword(String userId); + Mono lostPassword(String userEmail); + Mono setPassword(String userId, String password); Mono buildUserDetail(User user, boolean withoutDynamicGroups); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index e7526be8d..073f9644c 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -40,6 +40,8 @@ import javax.annotation.Nonnull; import java.security.SecureRandom; +import java.time.Duration; +import java.time.LocalDate; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -262,6 +264,14 @@ public Mono resetPassword(String userId) { }); } + @Override + public Mono lostPassword(String userEmail) { + return findByName(userEmail) + .flatMap(user -> { + return Mono.justOrEmpty(user.getName()); + }); + } + @SuppressWarnings("SpellCheckingInspection") @Nonnull private static String generateNewRandomPwd() { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 42161bd5a..5016bc73d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -65,6 +65,10 @@ public Mono resetPassword(String userId) { .then(userService.resetPassword(userId)); } + public Mono lostPassword(String userEmail) { + return userService.lostPassword(userEmail); + } + // ========================== TOKEN OPERATIONS START ========================== public Mono saveToken(String userId, String source, String token) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index d865cba4f..8eaeba8b0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -146,6 +146,15 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques } + @Override + public Mono> lostPassword(@RequestBody LostPasswordRequest request) { + if (StringUtils.isBlank(request.userEmail())) { + return ofError(BizError.INVALID_PARAMETER, "INVALID_USER_EMAIL"); + } + return userApiService.lostPassword(request.userEmail()) + .map(ResponseView::success); + } + @Override public Mono> setPassword(@RequestParam String password) { if (StringUtils.isBlank(password)) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index 96c93c472..54d9ff5b5 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -121,6 +121,9 @@ public interface UserEndpoints @PostMapping("/reset-password") public Mono> resetPassword(@RequestBody ResetPasswordRequest request); + @PostMapping("/lost-password") + public Mono> lostPassword(@RequestBody LostPasswordRequest userEmail); + @Operation( tags = TAG_USER_PASSWORD_MANAGEMENT, operationId = "setPassword", @@ -151,6 +154,9 @@ public interface UserEndpoints public record ResetPasswordRequest(String userId) { } + public record LostPasswordRequest(String userEmail) { + } + public record UpdatePasswordRequest(String oldPassword, String newPassword) { } From ceabd3bb03943d7e6c98a36cafc742e5a6956bd9 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Sun, 18 Feb 2024 00:09:48 +0500 Subject: [PATCH 02/33] feature: Initial Implementation of Email Sending Feature with dummy Data. --- server/api-service/lowcoder-domain/pom.xml | 4 ++ .../org/lowcoder/domain/user/model/User.java | 5 ++ .../service/EmailCommunicationService.java | 49 +++++++++++++++++++ .../domain/user/service/UserService.java | 2 +- .../domain/user/service/UserServiceImpl.java | 18 +++++-- .../org/lowcoder/sdk/config/CommonConfig.java | 8 +++ .../api/usermanagement/UserApiService.java | 2 +- .../api/usermanagement/UserController.java | 2 +- .../api/usermanagement/UserEndpoints.java | 2 +- .../main/resources/application-lowcoder.yml | 14 +++++- 10 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java diff --git a/server/api-service/lowcoder-domain/pom.xml b/server/api-service/lowcoder-domain/pom.xml index 2150c484a..8eec5df20 100644 --- a/server/api-service/lowcoder-domain/pom.xml +++ b/server/api-service/lowcoder-domain/pom.xml @@ -50,6 +50,10 @@ org.lowcoder lowcoder-infra + + org.springframework.boot + spring-boot-starter-mail + com.github.cloudyrock.mongock diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java index 80efd4ac5..a8350e0b5 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java @@ -3,6 +3,7 @@ import static com.google.common.base.Suppliers.memoize; import static org.lowcoder.infra.util.AssetUtils.toAssetPath; +import java.time.Instant; import java.util.*; import java.util.function.Supplier; @@ -52,6 +53,10 @@ public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterM @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; + private String passwordResetToken; + + private Instant passwordResetTokenExpiry; + @Transient Boolean isAnonymous = false; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java new file mode 100644 index 000000000..7b88c3104 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -0,0 +1,49 @@ +package org.lowcoder.domain.user.service; + +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Service +@Slf4j(topic = "EmailCommunicationService") +public class EmailCommunicationService { + + @Autowired + private JavaMailSender javaMailSender; + + @Value("${spring.mail.username}") + private String fromEmail; + + public boolean sendMail(String to, String token, String message) { + try { + String subject = "Lost Password Email"; + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); + + mimeMessageHelper.setFrom(fromEmail); + mimeMessageHelper.setTo(to); + mimeMessageHelper.setSubject(subject); + + // Construct the message with the token link + String resetLink = "http://localhost:8080/lost-password?token=" + token; + String messageWithLink = message + "\n\nReset your password here: " + resetLink; + mimeMessageHelper.setText(messageWithLink, true); // Set HTML to true to allow links + + javaMailSender.send(mimeMessage); + + return true; + + } catch (Exception e) { + log.error("Failed to send mail to: {}, Exception: {}", to, e); + return false; + } + + + } + +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index 497f50f1e..658e1e6a0 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -50,7 +50,7 @@ public interface UserService { Mono resetPassword(String userId); - Mono lostPassword(String userEmail); + Mono lostPassword(String userEmail); Mono setPassword(String userId, String password); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 073f9644c..5ee1aa9ae 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -29,6 +29,7 @@ import org.lowcoder.sdk.constants.WorkspaceMode; import org.lowcoder.sdk.exception.BizError; import org.lowcoder.sdk.exception.BizException; +import org.lowcoder.sdk.util.HashUtils; import org.lowcoder.sdk.util.LocaleUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; @@ -41,7 +42,9 @@ import javax.annotation.Nonnull; import java.security.SecureRandom; import java.time.Duration; +import java.time.Instant; import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -76,7 +79,8 @@ public class UserServiceImpl implements UserService { private CommonConfig commonConfig; @Autowired private AuthenticationService authenticationService; - + @Autowired + private EmailCommunicationService emailCommunicationService; private Conf avatarMaxSizeInKb; @PostConstruct @@ -265,10 +269,18 @@ public Mono resetPassword(String userId) { } @Override - public Mono lostPassword(String userEmail) { + public Mono lostPassword(String userEmail) { return findByName(userEmail) .flatMap(user -> { - return Mono.justOrEmpty(user.getName()); + String token = generateNewRandomPwd(); + Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS); + // TODO - IRFAN this is just a dummy email. + if (!emailCommunicationService.sendMail("notify@lowcoder.org", token, "Click Here")) { + return ofError(BizError.USER_NOT_EXIST, "SENDING_EMAIL_FAILED"); + } + user.setPasswordResetToken(HashUtils.hash(token.getBytes())); + user.setPasswordResetTokenExpiry(tokenExpiry); + return Mono.empty(); }); } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index 7d32ed0d8..06fcd486a 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -44,6 +44,7 @@ public class CommonConfig { private Cookie cookie = new Cookie(); private JsExecutor jsExecutor = new JsExecutor(); private Set disallowedHosts = new HashSet<>(); + private SMTP smtp = new SMTP(); public boolean isSelfHost() { return !isCloud(); @@ -145,6 +146,13 @@ public static class JsExecutor { private String host; } + @Data + public static class SMTP { + private String email; + private String host; + private String port; + } + @Getter @Setter diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 5016bc73d..751001cb3 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -65,7 +65,7 @@ public Mono resetPassword(String userId) { .then(userService.resetPassword(userId)); } - public Mono lostPassword(String userEmail) { + public Mono lostPassword(String userEmail) { return userService.lostPassword(userEmail); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index 8eaeba8b0..d045cbf0e 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -147,7 +147,7 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques } @Override - public Mono> lostPassword(@RequestBody LostPasswordRequest request) { + public Mono> lostPassword(@RequestBody LostPasswordRequest request) { if (StringUtils.isBlank(request.userEmail())) { return ofError(BizError.INVALID_PARAMETER, "INVALID_USER_EMAIL"); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index 54d9ff5b5..f65ded4a1 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -122,7 +122,7 @@ public interface UserEndpoints public Mono> resetPassword(@RequestBody ResetPasswordRequest request); @PostMapping("/lost-password") - public Mono> lostPassword(@RequestBody LostPasswordRequest userEmail); + public Mono> lostPassword(@RequestBody LostPasswordRequest userEmail); @Operation( tags = TAG_USER_PASSWORD_MANAGEMENT, diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 75ae0dba9..532554b9c 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -9,6 +9,18 @@ spring: main: allow-bean-definition-overriding: true allow-circular-references: true + mail: +# TODO - IRFAN this is just a test email to check the email sending. + username: "irfan.ayub@ikhwatech.com" + password: "abcd" + host: "smtp.freesmtpservers.com" + port: 25 +# properties: +# mail: +# smtp: +# starttls: +# enable: true +# auth: true server: compression: @@ -62,4 +74,4 @@ auth: email: enable: true enable-register: true - workspace-creation: true \ No newline at end of file + workspace-creation: true From c4b18bd65055c3a298b11faeadbbd33ee6cb8ffb Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Sun, 18 Feb 2024 13:51:04 +0500 Subject: [PATCH 03/33] feature: Initial Password reset flow implementation --- .../service/EmailCommunicationService.java | 2 +- .../domain/user/service/UserService.java | 2 ++ .../domain/user/service/UserServiceImpl.java | 25 ++++++++++++++++++- .../api/usermanagement/UserApiService.java | 4 +++ .../api/usermanagement/UserController.java | 11 ++++++++ .../api/usermanagement/UserEndpoints.java | 8 +++++- 6 files changed, 49 insertions(+), 3 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java index 7b88c3104..1a8c30896 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -30,7 +30,7 @@ public boolean sendMail(String to, String token, String message) { mimeMessageHelper.setSubject(subject); // Construct the message with the token link - String resetLink = "http://localhost:8080/lost-password?token=" + token; + String resetLink = "http://localhost:8080/api/users/lost-password/" + token; String messageWithLink = message + "\n\nReset your password here: " + resetLink; mimeMessageHelper.setText(messageWithLink, true); // Set HTML to true to allow links diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index 658e1e6a0..ec0e7e235 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -52,6 +52,8 @@ public interface UserService { Mono lostPassword(String userEmail); + Mono resetLostPassword(String userEmail, String token, String newPassword); + Mono setPassword(String userId, String password); Mono buildUserDetail(User user, boolean withoutDynamicGroups); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 5ee1aa9ae..1fe36c169 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -280,7 +280,30 @@ public Mono lostPassword(String userEmail) { } user.setPasswordResetToken(HashUtils.hash(token.getBytes())); user.setPasswordResetTokenExpiry(tokenExpiry); - return Mono.empty(); + return repository.save(user).then(Mono.empty()); + }); + } + + @Override + public Mono resetLostPassword(String userEmail, String token, String newPassword) { + return findByName(userEmail) + .flatMap(user -> { + if (Instant.now().until(user.getPasswordResetTokenExpiry(), ChronoUnit.MINUTES) <= 0) { + return ofError(BizError.LOGIN_EXPIRED, "TOKEN_EXPIRED"); + } + + if (!StringUtils.equals(HashUtils.hash(token.getBytes()), user.getPasswordResetToken())) { + return ofError(BizError.INVALID_PASSWORD, "INVALID_TOKEN"); + } + + if (StringUtils.isBlank(newPassword)) { + return ofError(BizError.INVALID_PASSWORD, "PASSWORD_NOT_SET_YET"); + } + + user.setPassword(encryptionService.encryptPassword(newPassword)); + user.setPasswordResetToken(StringUtils.EMPTY); + user.setPasswordResetTokenExpiry(Instant.now()); + return repository.save(user).then(Mono.empty()); }); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 751001cb3..e2b6e847c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -69,6 +69,10 @@ public Mono lostPassword(String userEmail) { return userService.lostPassword(userEmail); } + public Mono resetLostPassword(String userEmail, String token, String newPassword) { + return userService.resetLostPassword(userEmail, token, newPassword); + } + // ========================== TOKEN OPERATIONS START ========================== public Mono saveToken(String userId, String source, String token) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index d045cbf0e..02dca02ff 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -155,6 +155,17 @@ public Mono> lostPassword(@RequestBody LostPasswordRequest re .map(ResponseView::success); } + @Override + public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request) { + if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(token) + || StringUtils.isBlank(request.newPassword())) { + return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER"); + } + + return userApiService.resetLostPassword(request.userEmail(), token, request.newPassword()) + .map(ResponseView::success); + } + @Override public Mono> setPassword(@RequestParam String password) { if (StringUtils.isBlank(password)) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index f65ded4a1..e5d3f517c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -122,7 +122,10 @@ public interface UserEndpoints public Mono> resetPassword(@RequestBody ResetPasswordRequest request); @PostMapping("/lost-password") - public Mono> lostPassword(@RequestBody LostPasswordRequest userEmail); + public Mono> lostPassword(@RequestBody LostPasswordRequest request); + + @PostMapping("/lost-password/{token}") + public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request); @Operation( tags = TAG_USER_PASSWORD_MANAGEMENT, @@ -157,6 +160,9 @@ public record ResetPasswordRequest(String userId) { public record LostPasswordRequest(String userEmail) { } + public record ResetLostPasswordRequest(String userEmail, String newPassword) { + } + public record UpdatePasswordRequest(String oldPassword, String newPassword) { } From 96081d3691c022501790d374c6ade7231f7ea11d Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Wed, 21 Feb 2024 01:05:27 +0500 Subject: [PATCH 04/33] feature: Added email template in OrganizationCommonSettings, Added public url env variable and some code optimizations. --- .../organization/model/Organization.java | 2 +- .../service/EmailCommunicationService.java | 4 ++-- .../domain/user/service/UserService.java | 4 ++-- .../domain/user/service/UserServiceImpl.java | 24 +++++++++++++------ .../api/usermanagement/UserApiService.java | 4 ++-- .../api/usermanagement/UserController.java | 4 ++-- .../api/usermanagement/UserEndpoints.java | 4 ++-- .../main/resources/application-lowcoder.yml | 1 + 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java index 984788f5a..6b4d2b05c 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java @@ -86,7 +86,7 @@ public OrganizationCommonSettings getCommonSettings() { public static class OrganizationCommonSettings extends HashMap { public static final String USER_EXTRA_TRANSFORMER = "userExtraTransformer"; public static final String USER_EXTRA_TRANSFORMER_UPDATE_TIME = "userExtraTransformer_updateTime"; - + public static final String PASSWORD_RESET_EMAIL_TEMPLATE = "passwordRestEmailTemplate"; // custom branding configs public static final String CUSTOM_BRANDING_KEY = "branding"; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java index 1a8c30896..16fa8f196 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -20,7 +20,7 @@ public class EmailCommunicationService { public boolean sendMail(String to, String token, String message) { try { - String subject = "Lost Password Email"; + String subject = "Reset Your Password"; MimeMessage mimeMessage = javaMailSender.createMimeMessage(); MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); @@ -30,7 +30,7 @@ public boolean sendMail(String to, String token, String message) { mimeMessageHelper.setSubject(subject); // Construct the message with the token link - String resetLink = "http://localhost:8080/api/users/lost-password/" + token; + String resetLink = lowcoderPublicUrl + "/api/users/lost-password/" + token; String messageWithLink = message + "\n\nReset your password here: " + resetLink; mimeMessageHelper.setText(messageWithLink, true); // Set HTML to true to allow links diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index ec0e7e235..667f48c44 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -50,9 +50,9 @@ public interface UserService { Mono resetPassword(String userId); - Mono lostPassword(String userEmail); + Mono lostPassword(String userEmail); - Mono resetLostPassword(String userEmail, String token, String newPassword); + Mono resetLostPassword(String userEmail, String token, String newPassword); Mono setPassword(String userId, String password); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 1fe36c169..62e2f8e3b 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -16,6 +16,7 @@ import org.lowcoder.domain.group.service.GroupService; import org.lowcoder.domain.organization.model.OrgMember; import org.lowcoder.domain.organization.service.OrgMemberService; +import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.domain.user.model.*; import org.lowcoder.domain.user.model.User.TransformedUserInfo; import org.lowcoder.domain.user.repository.UserRepository; @@ -74,6 +75,8 @@ public class UserServiceImpl implements UserService { @Autowired private OrgMemberService orgMemberService; @Autowired + private OrganizationService organizationService; + @Autowired private GroupService groupService; @Autowired private CommonConfig commonConfig; @@ -269,14 +272,20 @@ public Mono resetPassword(String userId) { } @Override - public Mono lostPassword(String userEmail) { + public Mono lostPassword(String userEmail) { return findByName(userEmail) - .flatMap(user -> { + .zipWhen(user -> orgMemberService.getCurrentOrgMember(user.getId()) + .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) + .map(organization -> organization.getCommonSettings().get("PASSWORD_RESET_EMAIL_TEMPLATE"))) + .flatMap(tuple -> { + User user = tuple.getT1(); + String emailTemplate = (String)tuple.getT2(); + String token = generateNewRandomPwd(); Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS); - // TODO - IRFAN this is just a dummy email. - if (!emailCommunicationService.sendMail("notify@lowcoder.org", token, "Click Here")) { - return ofError(BizError.USER_NOT_EXIST, "SENDING_EMAIL_FAILED"); + + if (!emailCommunicationService.sendMail(userEmail, token, emailTemplate)) { + return ofError(BizError.AUTH_ERROR, "SENDING_EMAIL_FAILED"); } user.setPasswordResetToken(HashUtils.hash(token.getBytes())); user.setPasswordResetTokenExpiry(tokenExpiry); @@ -285,7 +294,7 @@ public Mono lostPassword(String userEmail) { } @Override - public Mono resetLostPassword(String userEmail, String token, String newPassword) { + public Mono resetLostPassword(String userEmail, String token, String newPassword) { return findByName(userEmail) .flatMap(user -> { if (Instant.now().until(user.getPasswordResetTokenExpiry(), ChronoUnit.MINUTES) <= 0) { @@ -303,7 +312,8 @@ public Mono resetLostPassword(String userEmail, String token, String newPa user.setPassword(encryptionService.encryptPassword(newPassword)); user.setPasswordResetToken(StringUtils.EMPTY); user.setPasswordResetTokenExpiry(Instant.now()); - return repository.save(user).then(Mono.empty()); + return repository.save(user) + .thenReturn(true); }); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index e2b6e847c..619ca5b00 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -65,11 +65,11 @@ public Mono resetPassword(String userId) { .then(userService.resetPassword(userId)); } - public Mono lostPassword(String userEmail) { + public Mono lostPassword(String userEmail) { return userService.lostPassword(userEmail); } - public Mono resetLostPassword(String userEmail, String token, String newPassword) { + public Mono resetLostPassword(String userEmail, String token, String newPassword) { return userService.resetLostPassword(userEmail, token, newPassword); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index 02dca02ff..feac3deb2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -147,7 +147,7 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques } @Override - public Mono> lostPassword(@RequestBody LostPasswordRequest request) { + public Mono> lostPassword(@RequestBody LostPasswordRequest request) { if (StringUtils.isBlank(request.userEmail())) { return ofError(BizError.INVALID_PARAMETER, "INVALID_USER_EMAIL"); } @@ -156,7 +156,7 @@ public Mono> lostPassword(@RequestBody LostPasswordRequest re } @Override - public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request) { + public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request) { if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(token) || StringUtils.isBlank(request.newPassword())) { return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index e5d3f517c..62939c471 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -122,10 +122,10 @@ public interface UserEndpoints public Mono> resetPassword(@RequestBody ResetPasswordRequest request); @PostMapping("/lost-password") - public Mono> lostPassword(@RequestBody LostPasswordRequest request); + public Mono> lostPassword(@RequestBody LostPasswordRequest request); @PostMapping("/lost-password/{token}") - public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request); + public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request); @Operation( tags = TAG_USER_PASSWORD_MANAGEMENT, diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 532554b9c..c217ce86e 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -56,6 +56,7 @@ common: block-hound-enable: false js-executor: host: http://127.0.0.1:6060 + lowcoder_public_url: http://localhost:8080 material: mongodb-grid-fs: From d5c63ea1c953ce0a46801bc21d21d53b97d9d8cd Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Thu, 22 Feb 2024 20:59:03 +0500 Subject: [PATCH 05/33] feature: Added emailTemplate in common-settings. --- .../organization/service/OrganizationServiceImpl.java | 9 +++++++++ .../domain/user/service/EmailCommunicationService.java | 9 ++++++--- .../lowcoder/domain/user/service/UserServiceImpl.java | 5 +---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 48e4bc6de..c102ff1b0 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -1,6 +1,7 @@ package org.lowcoder.domain.organization.service; import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG; +import static org.lowcoder.domain.organization.model.Organization.OrganizationCommonSettings.PASSWORD_RESET_EMAIL_TEMPLATE; import static org.lowcoder.domain.organization.model.OrganizationState.ACTIVE; import static org.lowcoder.domain.organization.model.OrganizationState.DELETED; import static org.lowcoder.domain.util.QueryDslUtils.fieldName; @@ -56,6 +57,12 @@ public class OrganizationServiceImpl implements OrganizationService { private final Conf logoMaxSizeInKb; + private static final String PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT = "

Hi, %s
" + + "Here is the link to reset your password: %s
" + + "Please note that the link will expire after 12 hours.

" + + "Regards,
" + + "The Lowcoder Team

"; + @Autowired private AssetRepository assetRepository; @@ -151,6 +158,8 @@ public Mono create(Organization organization, String creatorId) { if (organization == null || StringUtils.isNotBlank(organization.getId())) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ORGANIZATION)); } + organization.getCommonSettings().put(PASSWORD_RESET_EMAIL_TEMPLATE, + PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT); organization.setState(ACTIVE); return Mono.just(organization); }) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java index 16fa8f196..8373b60fc 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -18,7 +18,10 @@ public class EmailCommunicationService { @Value("${spring.mail.username}") private String fromEmail; - public boolean sendMail(String to, String token, String message) { + @Value("${common.lowcoder_public_url}") + private String lowcoderPublicUrl; + + public boolean sendPasswordResetEmail(String to, String token, String message) { try { String subject = "Reset Your Password"; MimeMessage mimeMessage = javaMailSender.createMimeMessage(); @@ -31,8 +34,8 @@ public boolean sendMail(String to, String token, String message) { // Construct the message with the token link String resetLink = lowcoderPublicUrl + "/api/users/lost-password/" + token; - String messageWithLink = message + "\n\nReset your password here: " + resetLink; - mimeMessageHelper.setText(messageWithLink, true); // Set HTML to true to allow links + String formattedMessage = String.format(message, to, resetLink); + mimeMessageHelper.setText(formattedMessage, true); // Set HTML to true to allow links javaMailSender.send(mimeMessage); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 62e2f8e3b..7819779aa 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -42,9 +42,7 @@ import javax.annotation.Nonnull; import java.security.SecureRandom; -import java.time.Duration; import java.time.Instant; -import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.function.Function; @@ -283,8 +281,7 @@ public Mono lostPassword(String userEmail) { String token = generateNewRandomPwd(); Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS); - - if (!emailCommunicationService.sendMail(userEmail, token, emailTemplate)) { + if (!emailCommunicationService.sendPasswordResetEmail(userEmail, token, emailTemplate)) { return ofError(BizError.AUTH_ERROR, "SENDING_EMAIL_FAILED"); } user.setPasswordResetToken(HashUtils.hash(token.getBytes())); From 65d9e2a0af759cca84bceef35cf0cdf8b6925f1c Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Mon, 26 Feb 2024 02:12:20 +0500 Subject: [PATCH 06/33] Misc fixes --- .../user/service/EmailCommunicationService.java | 17 +++++++---------- .../org/lowcoder/sdk/config/CommonConfig.java | 10 ++-------- .../api/framework/security/SecurityConfig.java | 2 ++ .../api/usermanagement/UserController.java | 2 +- .../src/main/resources/application-lowcoder.yml | 14 +++----------- 5 files changed, 15 insertions(+), 30 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java index 8373b60fc..81f91756f 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -2,8 +2,8 @@ import jakarta.mail.internet.MimeMessage; import lombok.extern.slf4j.Slf4j; +import org.lowcoder.sdk.config.CommonConfig; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; @@ -15,25 +15,22 @@ public class EmailCommunicationService { @Autowired private JavaMailSender javaMailSender; - @Value("${spring.mail.username}") - private String fromEmail; - - @Value("${common.lowcoder_public_url}") - private String lowcoderPublicUrl; + @Autowired + private CommonConfig config; public boolean sendPasswordResetEmail(String to, String token, String message) { try { - String subject = "Reset Your Password"; + String subject = "Reset Your Lost Password"; MimeMessage mimeMessage = javaMailSender.createMimeMessage(); MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); - mimeMessageHelper.setFrom(fromEmail); + mimeMessageHelper.setFrom(config.getLostPasswordEmailSender()); mimeMessageHelper.setTo(to); mimeMessageHelper.setSubject(subject); // Construct the message with the token link - String resetLink = lowcoderPublicUrl + "/api/users/lost-password/" + token; + String resetLink = config.getLowcoderPublicUrl() + "lost-password?token=" + token; String formattedMessage = String.format(message, to, resetLink); mimeMessageHelper.setText(formattedMessage, true); // Set HTML to true to allow links @@ -42,7 +39,7 @@ public boolean sendPasswordResetEmail(String to, String token, String message) { return true; } catch (Exception e) { - log.error("Failed to send mail to: {}, Exception: {}", to, e); + log.error("Failed to send mail to: {}, Exception: ", to, e); return false; } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index 2186b8101..08f8730f5 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -45,7 +45,8 @@ public class CommonConfig { private JsExecutor jsExecutor = new JsExecutor(); private Set disallowedHosts = new HashSet<>(); private Marketplace marketplace = new Marketplace(); - private SMTP smtp = new SMTP(); + private String lowcoderPublicUrl; + private String lostPasswordEmailSender; public boolean isSelfHost() { return !isCloud(); @@ -147,13 +148,6 @@ public static class JsExecutor { private String host; } - @Data - public static class SMTP { - private String email; - private String host; - private String port; - } - @Data public static class Marketplace { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index b933a63e1..9b1b99a0c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -113,6 +113,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/me"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/currentUser"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/lost-password"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, GROUP_URL + "/list"), // application view ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, QUERY_URL + "/execute"), // application view @@ -139,6 +140,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.APPLICATION_URL + "/marketplace-apps"), // marketplace apps ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/me"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/currentUser"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/lost-password"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.GROUP_URL + "/list"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.QUERY_URL + "/execute"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.MATERIAL_URL + "/**"), diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index feac3deb2..970f129ae 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -149,7 +149,7 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques @Override public Mono> lostPassword(@RequestBody LostPasswordRequest request) { if (StringUtils.isBlank(request.userEmail())) { - return ofError(BizError.INVALID_PARAMETER, "INVALID_USER_EMAIL"); + return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER"); } return userApiService.lostPassword(request.userEmail()) .map(ResponseView::success); diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index b08b9680f..b91887386 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -10,17 +10,8 @@ spring: allow-bean-definition-overriding: true allow-circular-references: true mail: -# TODO - IRFAN this is just a test email to check the email sending. - username: "irfan.ayub@ikhwatech.com" - password: "abcd" - host: "smtp.freesmtpservers.com" + host: smtp.freesmtpservers.com port: 25 -# properties: -# mail: -# smtp: -# starttls: -# enable: true -# auth: true server: compression: @@ -58,7 +49,8 @@ common: host: http://127.0.0.1:6060 marketplace: private-mode: false - lowcoder_public_url: http://localhost:8080 + lowcoder-public-url: http://localhost:8080 + lost-password-email-sender: business@ikhwatech.com material: mongodb-grid-fs: From 39ad3ac732ff15948ff51d27db66945908164487 Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Mon, 26 Feb 2024 19:15:29 +0500 Subject: [PATCH 07/33] Misc fixes --- .../lowcoder/domain/organization/model/Organization.java | 2 +- .../organization/service/OrganizationServiceImpl.java | 3 ++- .../domain/user/service/EmailCommunicationService.java | 2 +- .../org/lowcoder/domain/user/service/UserServiceImpl.java | 8 ++------ .../lowcoder-sdk/src/main/resources/locale_en.properties | 3 +++ .../lowcoder/api/framework/security/SecurityConfig.java | 2 ++ .../org/lowcoder/api/usermanagement/UserController.java | 6 +++--- .../org/lowcoder/api/usermanagement/UserEndpoints.java | 6 +++--- 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java index 6b4d2b05c..21584ba8f 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java @@ -86,7 +86,7 @@ public OrganizationCommonSettings getCommonSettings() { public static class OrganizationCommonSettings extends HashMap { public static final String USER_EXTRA_TRANSFORMER = "userExtraTransformer"; public static final String USER_EXTRA_TRANSFORMER_UPDATE_TIME = "userExtraTransformer_updateTime"; - public static final String PASSWORD_RESET_EMAIL_TEMPLATE = "passwordRestEmailTemplate"; + public static final String PASSWORD_RESET_EMAIL_TEMPLATE = "passwordResetEmailTemplate"; // custom branding configs public static final String CUSTOM_BRANDING_KEY = "branding"; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index c102ff1b0..fb1e55db5 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -158,7 +158,8 @@ public Mono create(Organization organization, String creatorId) { if (organization == null || StringUtils.isNotBlank(organization.getId())) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ORGANIZATION)); } - organization.getCommonSettings().put(PASSWORD_RESET_EMAIL_TEMPLATE, + organization.setCommonSettings(new OrganizationCommonSettings()); + organization.getCommonSettings().put("PASSWORD_RESET_EMAIL_TEMPLATE", PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT); organization.setState(ACTIVE); return Mono.just(organization); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java index 81f91756f..439aa65a8 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -30,7 +30,7 @@ public boolean sendPasswordResetEmail(String to, String token, String message) { mimeMessageHelper.setSubject(subject); // Construct the message with the token link - String resetLink = config.getLowcoderPublicUrl() + "lost-password?token=" + token; + String resetLink = config.getLowcoderPublicUrl() + "/lost-password?token=" + token; String formattedMessage = String.format(message, to, resetLink); mimeMessageHelper.setText(formattedMessage, true); // Set HTML to true to allow links diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 7819779aa..677a15a49 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -295,15 +295,11 @@ public Mono resetLostPassword(String userEmail, String token, String ne return findByName(userEmail) .flatMap(user -> { if (Instant.now().until(user.getPasswordResetTokenExpiry(), ChronoUnit.MINUTES) <= 0) { - return ofError(BizError.LOGIN_EXPIRED, "TOKEN_EXPIRED"); + return ofError(BizError.INVALID_PARAMETER, "TOKEN_EXPIRED"); } if (!StringUtils.equals(HashUtils.hash(token.getBytes()), user.getPasswordResetToken())) { - return ofError(BizError.INVALID_PASSWORD, "INVALID_TOKEN"); - } - - if (StringUtils.isBlank(newPassword)) { - return ofError(BizError.INVALID_PASSWORD, "PASSWORD_NOT_SET_YET"); + return ofError(BizError.INVALID_PARAMETER, "INVALID_TOKEN"); } user.setPassword(encryptionService.encryptPassword(newPassword)); diff --git a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties index d81ecbdf2..1e3a18c31 100644 --- a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties +++ b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties @@ -17,6 +17,9 @@ CANNOT_DELETE_SYSTEM_GROUP=System group cannot be deleted. NEED_DEV_TO_CREATE_RESOURCE=Invalid operation, workspace developers or admin required. UNABLE_TO_FIND_VALID_ORG=Cannot find a valid workspace for current user. USER_BANNED=Current account is frozen. +SENDING_EMAIL_FAILED=Email could not be sent. Please check the smtp settings for the org. +TOKEN_EXPIRED=Token to reset the password has expired +INVALID_TOKEN=Invalid token received for password reset request # invitation INVALID_INVITATION_CODE=Invitation code not found. ALREADY_IN_ORGANIZATION=You are already in this workspace. diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index 9b1b99a0c..82174984f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -114,6 +114,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/me"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/currentUser"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/lost-password"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/reset-lost-password"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, GROUP_URL + "/list"), // application view ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, QUERY_URL + "/execute"), // application view @@ -141,6 +142,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/me"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/currentUser"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/lost-password"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/reset-lost-password"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.GROUP_URL + "/list"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.QUERY_URL + "/execute"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.MATERIAL_URL + "/**"), diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index 970f129ae..ee4aac0bb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -156,13 +156,13 @@ public Mono> lostPassword(@RequestBody LostPasswordRequest } @Override - public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request) { - if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(token) + public Mono> resetLostPassword(@RequestBody ResetLostPasswordRequest request) { + if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(request.token()) || StringUtils.isBlank(request.newPassword())) { return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER"); } - return userApiService.resetLostPassword(request.userEmail(), token, request.newPassword()) + return userApiService.resetLostPassword(request.userEmail(), request.token(), request.newPassword()) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index 62939c471..196925134 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -124,8 +124,8 @@ public interface UserEndpoints @PostMapping("/lost-password") public Mono> lostPassword(@RequestBody LostPasswordRequest request); - @PostMapping("/lost-password/{token}") - public Mono> resetLostPassword(@PathVariable String token, @RequestBody ResetLostPasswordRequest request); + @PostMapping("/reset-lost-password") + public Mono> resetLostPassword(@RequestBody ResetLostPasswordRequest request); @Operation( tags = TAG_USER_PASSWORD_MANAGEMENT, @@ -160,7 +160,7 @@ public record ResetPasswordRequest(String userId) { public record LostPasswordRequest(String userEmail) { } - public record ResetLostPasswordRequest(String userEmail, String newPassword) { + public record ResetLostPasswordRequest(String token, String userEmail, String newPassword) { } public record UpdatePasswordRequest(String oldPassword, String newPassword) { From 72518dc21c84238e9b0242e278d96f65c4547570 Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Mon, 26 Feb 2024 19:19:26 +0500 Subject: [PATCH 08/33] Update application.props --- .../lowcoder-server/src/main/resources/application-lowcoder.yml | 2 +- .../src/main/resources/selfhost/ce/application.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index b91887386..31774d18d 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -50,7 +50,7 @@ common: marketplace: private-mode: false lowcoder-public-url: http://localhost:8080 - lost-password-email-sender: business@ikhwatech.com + lost-password-email-sender: info@lowcoder.org material: mongodb-grid-fs: diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 11c0511c2..b8c979354 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -55,6 +55,8 @@ common: mode: ${LOWCODER_WORKSPACE_MODE:SAAS} marketplace: private-mode: ${MARKETPLACE_PRIVATE_MODE:true} + lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:8080} + lost-password-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:info@lowcoder.org} material: mongodb-grid-fs: From d3844185a384dd7864b3db5da3051fb81fcfeb7c Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Tue, 27 Feb 2024 00:27:35 +0500 Subject: [PATCH 09/33] Update application.props to include smtp server auth --- .../src/main/resources/application-lowcoder.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 31774d18d..61c9f527f 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -10,8 +10,19 @@ spring: allow-bean-definition-overriding: true allow-circular-references: true mail: - host: smtp.freesmtpservers.com - port: 25 + host: smtp.gmail.com + port: 587 + username: yourmail@gmail.com + password: yourpass + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + transport: + protocol: smtp server: compression: From 0391d2a1bf23560d8ff712889ff1dd791ac91be7 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Tue, 27 Feb 2024 17:51:15 +0500 Subject: [PATCH 10/33] feature: Added Env Variables for SMPT server --- .../src/main/resources/selfhost/ce/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 15b146493..a0a328f0d 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -18,6 +18,11 @@ spring: max-in-memory-size: 20MB webflux: context-path: / + mail: + host: ${LOWCODER_SMTP_HOST:smtp.gmail.com} + port: ${LOWCODER_SMTP_PORT:587} + username: ${LOWCODER_SMTP_USERNAME:yourmail@gmail.com} + password: ${LOWCODER_SMTP_PASSWORD:yourpass} server: compression: From bc74e8ac373de7b1291223e23c0b8104898b8261 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Tue, 27 Feb 2024 17:56:23 +0500 Subject: [PATCH 11/33] feature: Rename SMTP Env variable to ADMIN --- .../src/main/resources/selfhost/ce/application.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index a0a328f0d..ca9b5b06a 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -19,10 +19,10 @@ spring: webflux: context-path: / mail: - host: ${LOWCODER_SMTP_HOST:smtp.gmail.com} - port: ${LOWCODER_SMTP_PORT:587} - username: ${LOWCODER_SMTP_USERNAME:yourmail@gmail.com} - password: ${LOWCODER_SMTP_PASSWORD:yourpass} + host: ${LOWCODER_ADMIN_SMTP_HOST:smtp.gmail.com} + port: ${LOWCODER_ADMIN_SMTP_PORT:587} + username: ${LOWCODER_ADMIN_SMTP_USERNAME:yourmail@gmail.com} + password: ${LOWCODER_ADMIN_SMTP_PASSWORD:yourpass} server: compression: From 345c18af60653a23b290039929d11f9543832d9e Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Thu, 29 Feb 2024 22:39:25 +0500 Subject: [PATCH 12/33] Label style work in progress --- .../src/components/Section.tsx | 1 + .../src/i18n/design/locales/en.ts | 1 + .../src/i18n/design/locales/zh.ts | 1 + .../comps/comps/textInputComp/inputComp.tsx | 32 ++++++----- .../textInputComp/textInputConstants.tsx | 4 +- .../src/comps/controls/labelControl.tsx | 55 ++++++++++++------- .../src/comps/controls/styleControl.tsx | 22 ++++++++ .../comps/controls/styleControlConstants.tsx | 28 ++++++++-- .../packages/lowcoder/src/i18n/locales/de.ts | 1 + .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../packages/lowcoder/src/i18n/locales/zh.ts | 1 + 11 files changed, 105 insertions(+), 42 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/Section.tsx b/client/packages/lowcoder-design/src/components/Section.tsx index 869c0eabf..5b2d70b12 100644 --- a/client/packages/lowcoder-design/src/components/Section.tsx +++ b/client/packages/lowcoder-design/src/components/Section.tsx @@ -142,6 +142,7 @@ export const sectionNames = { validation: trans("prop.validation"), layout: trans("prop.layout"), style: trans("prop.style"), + labelStyle:trans("prop.labelStyle"), data: trans("prop.data"), meetings : trans("prop.meetings"), // added by Falk Wolsky }; diff --git a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts index c6fa81f69..543bb813a 100644 --- a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts +++ b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts @@ -22,6 +22,7 @@ export const en = { advanced: "Advanced", validation: "Validation", layout: "Layout", + labelStyle:"Label Style", style: "Style", meetings : "Meeting Settings", data: "Data", diff --git a/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts b/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts index 3fe60174b..a3622dca0 100644 --- a/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts +++ b/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts @@ -22,6 +22,7 @@ export const zh = { advanced: "高级", validation: "验证", layout: "布局", + labelStyle:"标签样式", style: "样式", meetings: "会议", data: "数据", diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx index fc34bc723..7c89af87f 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx @@ -1,7 +1,7 @@ import { Input, Section, sectionNames } from "lowcoder-design"; import { BoolControl } from "comps/controls/boolControl"; import { styleControl } from "comps/controls/styleControl"; -import { InputLikeStyle, InputLikeStyleType } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle, LabelStyleType } from "comps/controls/styleControlConstants"; import { NameConfig, NameConfigPlaceHolder, @@ -38,7 +38,7 @@ import { EditorContext } from "comps/editorState"; * Input Comp */ -const InputStyle = styled(Input)<{ $style: InputLikeStyleType }>` +const InputStyle = styled(Input) <{ $style: InputLikeStyleType}>` ${(props) => props.$style && getStyle(props.$style)} `; @@ -48,6 +48,7 @@ const childrenMap = { showCount: BoolControl, allowClear: BoolControl, style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle), prefixIcon: IconControl, suffixIcon: IconControl, }; @@ -67,7 +68,7 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { suffix={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), - style: props.style, + style: props.labelStyle, ...validateState, }); }) @@ -80,22 +81,25 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( children.label.getPropertyView() )} - + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <> -
{hiddenPropertyView(children)}
-
- {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} - {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })} - {children.showCount.propertyView({ label: trans("prop.showCount") })} - {allowClearPropertyView(children)} - {readOnlyPropertyView(children)} -
- +
{hiddenPropertyView(children)}
+
+ {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} + {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })} + {children.showCount.propertyView({ label: trans("prop.showCount") })} + {allowClearPropertyView(children)} + {readOnlyPropertyView(children)} +
+ )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( - <>
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 772cbdc51..6773a23f4 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -9,7 +9,7 @@ import { } from "comps/controls/codeControl"; import { stringExposingStateControl } from "comps/controls/codeStateControl"; import { LabelControl } from "comps/controls/labelControl"; -import { InputLikeStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; +import { InputLikeStyleType, LabelStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { Section, sectionNames, ValueFromOption } from "lowcoder-design"; import _ from "lodash"; import { css } from "styled-components"; @@ -217,7 +217,7 @@ export const TextInputValidationSection = (children: TextInputComp) => ( ); -export function getStyle(style: InputLikeStyleType) { +export function getStyle(style: InputLikeStyleType, labelStyle?: LabelStyleType) { return css` border-radius: ${style.radius}; border-width: ${style.borderWidth}; diff --git a/client/packages/lowcoder/src/comps/controls/labelControl.tsx b/client/packages/lowcoder/src/comps/controls/labelControl.tsx index 74a4f26ac..a2fe0947e 100644 --- a/client/packages/lowcoder/src/comps/controls/labelControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/labelControl.tsx @@ -9,13 +9,13 @@ import { MultiCompBuilder } from "comps/generators/multi"; import { labelCss, Section, Tooltip, UnderlineCss } from "lowcoder-design"; import { ValueFromOption } from "lowcoder-design"; import { isEmpty } from "lodash"; -import { ReactNode } from "react"; +import { Fragment, ReactNode } from "react"; import styled, { css } from "styled-components"; import { AlignLeft } from "lowcoder-design"; import { AlignRight } from "lowcoder-design"; import { StarIcon } from "lowcoder-design"; -import { heightCalculator, widthCalculator } from "./styleControlConstants"; +import { LabelStyleType, heightCalculator, widthCalculator } from "./styleControlConstants"; type LabelViewProps = Pick & { children: ReactNode; @@ -75,10 +75,20 @@ const LabelWrapper = styled.div<{ max-width: ${(props) => (props.$position === "row" ? "80%" : "100%")}; flex-shrink: 0; `; - -const Label = styled.span<{ $border: boolean }>` +// ${(props) => props.$border && UnderlineCss}; +const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType }>` ${labelCss}; - ${(props) => props.$border && UnderlineCss}; + + font-family:${(props) => props.$labelStyle.fontFamily}; + font-weight:${(props) => props.$labelStyle.fontWeight}; + font-style:${(props) => props.$labelStyle.fontStyle}; + text-transform:${(props) => props.$labelStyle.textTransform}; + text-decoration:${(props) => props.$labelStyle.textDecoration}; + font-size:${(props) => props.$labelStyle.textSize}; + color:${(props) => props.$labelStyle.text}; + ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${props.$labelStyle.border};`} + border-radius:${(props) => props.$labelStyle.radius}; + padding:${(props) => props.$labelStyle.padding}; width: fit-content; user-select: text; white-space: nowrap; @@ -144,21 +154,22 @@ export const LabelControl = (function () { position: dropdownControl(PositionOptions, "row"), align: dropdownControl(AlignOptions, "left"), }; + return new MultiCompBuilder(childrenMap, (props) => (args: LabelViewProps) => ( - {!props.hidden && !isEmpty(props.text) && ( node.closest(".react-grid-item")} > - + {args.required && } @@ -210,8 +225,8 @@ export const LabelControl = (function () { args.validateStatus === "error" ? red.primary : args.validateStatus === "warning" - ? yellow.primary - : green.primary + ? yellow.primary + : green.primary } > {args.help} diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index 109801a97..e9ad4b6d9 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -550,6 +550,7 @@ export function styleControl(colorConfig name === "textTransform" || name === "textDecoration" || name === "fontFamily" || + name === "borderStyle" || name === "fontStyle" || name === "backgroundImage" || name === "backgroundImageRepeat" || @@ -688,6 +689,13 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], + }): name === "borderStyle" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], }) : name === "margin" ? ( @@ -731,6 +739,20 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], + }): name === "textDecoration" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }): name === "textTransform" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], }) : name === "fontStyle" ? ( diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index e36cd9d9b..334270500 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -55,6 +55,10 @@ export type FontStyleConfig = CommonColorConfig & { readonly fontStyle: string; } +export type borderStyleConfig = CommonColorConfig & { + readonly borderStyle: string; +} + export type ContainerHeaderPaddigConfig = CommonColorConfig & { readonly containerheaderpadding: string; }; @@ -88,7 +92,7 @@ export type DepColorConfig = CommonColorConfig & { readonly depType?: DEP_TYPE; transformer: (color: string, ...rest: string[]) => string; }; -export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; +export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | borderStyleConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; export const defaultTheme: ThemeDetail = { primary: "#3377FF", @@ -399,13 +403,19 @@ const TEXT_TRANSFORM = { name: "textTransform", label: trans("style.textTransform"), textTransform: "textTransform" -} +} as const; const TEXT_DECORATION = { name: "textDecoration", label: trans("style.textDecoration"), textDecoration: "textDecoration" -} +} as const; + +const BORDER_STYLE = { + name: "borderStyle", + label: trans("style.borderStyle"), + borderStyle: "borderStyle" +} as const; const getStaticBorder = (color: string = SECOND_SURFACE_COLOR) => ({ @@ -432,6 +442,7 @@ const STYLING_FIELDS_SEQUENCE = [ FONT_FAMILY, FONT_STYLE, BORDER, + BORDER_STYLE, MARGIN, PADDING, RADIUS, @@ -716,12 +727,16 @@ export const SliderStyle = [ ] as const; export const InputLikeStyle = [ - LABEL, + // LABEL, getStaticBackground(SURFACE_COLOR), ...STYLING_FIELDS_SEQUENCE, ...ACCENT_VALIDATE, ] as const; +export const LabelStyle = [ + ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius') +] + export const RatingStyle = [ LABEL, { @@ -800,7 +815,7 @@ export const MultiSelectStyle = [ export const TabContainerStyle = [ // Keep background related properties of container as STYLING_FIELDS_SEQUENCE has rest of the properties - ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style)=> ['border','radius','borderWidth','margin','padding'].includes(style.name) === false),...STYLING_FIELDS_SEQUENCE], 'text', [{ + ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style) => ['border', 'radius', 'borderWidth', 'margin', 'padding'].includes(style.name) === false), ...STYLING_FIELDS_SEQUENCE], 'text', [{ name: "tabText", label: trans("style.tabText"), depName: "headerBackground", @@ -888,7 +903,7 @@ export const RadioStyle = [ export const SegmentStyle = [ LABEL, - ...STYLING_FIELDS_SEQUENCE.filter((style)=> ['border','borderWidth'].includes(style.name) === false), + ...STYLING_FIELDS_SEQUENCE.filter((style) => ['border', 'borderWidth'].includes(style.name) === false), { name: "indicatorBackground", label: trans("style.indicatorBackground"), @@ -1361,6 +1376,7 @@ export const RichTextEditorStyle = [ BORDER_WIDTH ] as const; +export type LabelStyleType = StyleConfigType; export type InputLikeStyleType = StyleConfigType; export type ButtonStyleType = StyleConfigType; export type ToggleButtonStyleType = StyleConfigType; diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index 404520788..dbe3f32e4 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -301,6 +301,7 @@ export const de = { "border": "Farbe der Umrandung", "borderRadius": "Radius der Grenze", "borderWidth": "Breite des Randes", + "borderStyle":"Grenzstil", "background": "Hintergrund", "headerBackground": "Kopfzeile Hintergrund", "footerBackground": "Fußzeilen-Hintergrund", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index a01b2c5e9..75985f8c7 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -316,6 +316,7 @@ export const en = { "border": "Border Color", "borderRadius": "Border Radius", "borderWidth": "Border Width", + "borderStyle":"Border Style", "background": "Background", "headerBackground": "Header Background", "footerBackground": "Footer Background", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index ca3e99f49..d40474c2a 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -307,6 +307,7 @@ style: { border: "边框颜色", borderRadius: "边框半径", borderWidth: "边框宽度", + borderStyle:"边框样式", background: "背景", headerBackground: "头部背景", footerBackground: "底部背景", From 1f164533ea486e9128094289b9dcb0f4a72b4f23 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Fri, 1 Mar 2024 22:56:13 +0500 Subject: [PATCH 13/33] Label style w.i.p --- .../autoCompleteComp/autoCompleteComp.tsx | 9 ++++-- .../comps/numberInputComp/numberInputComp.tsx | 10 ++++-- .../comps/numberInputComp/rangeSliderComp.tsx | 2 +- .../comps/numberInputComp/sliderComp.tsx | 2 +- .../numberInputComp/sliderCompConstants.tsx | 6 +++- .../comps/selectInputComp/cascaderComp.tsx | 2 +- .../selectInputComp/cascaderContants.tsx | 18 +++++++---- .../comps/selectInputComp/checkboxComp.tsx | 5 +-- .../comps/selectInputComp/multiSelectComp.tsx | 5 +-- .../comps/comps/selectInputComp/radioComp.tsx | 2 +- .../selectInputComp/radioCompConstants.tsx | 6 +++- .../comps/selectInputComp/selectComp.tsx | 7 ++-- .../selectInputComp/selectCompConstants.tsx | 14 +++++--- .../lowcoder/src/comps/comps/switchComp.tsx | 16 +++++++--- .../comps/textInputComp/passwordComp.tsx | 30 +++++++++-------- .../comps/textInputComp/textAreaComp.tsx | 32 +++++++++++-------- .../src/comps/comps/treeComp/treeComp.tsx | 16 ++++++---- .../comps/comps/treeComp/treeSelectComp.tsx | 13 ++++---- .../src/comps/controls/labelControl.tsx | 8 ++--- .../comps/controls/styleControlConstants.tsx | 6 ++-- 20 files changed, 131 insertions(+), 78 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index 059dd3852..d052bf4cb 100644 --- a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx @@ -5,6 +5,7 @@ import { styleControl } from "comps/controls/styleControl"; import { InputLikeStyle, InputLikeStyleType, + LabelStyle, } from "comps/controls/styleControlConstants"; import { NameConfig, @@ -73,6 +74,7 @@ const childrenMap = { viewRef: RefControl, allowClear: BoolControl.DEFAULT_TRUE, style: styleControl(InputLikeStyle), + labelStyle:styleControl(LabelStyle), prefixIcon: IconControl, suffixIcon: IconControl, items: jsonControl(convertAutoCompleteData, autoCompleteDate), @@ -276,8 +278,8 @@ let AutoCompleteCompBase = (function () { ), - // style: props.style, - // ...validateState, + style: props.labelStyle, + ...validateState, }); }) .setPropertyViewFn((children) => { @@ -335,6 +337,9 @@ let AutoCompleteCompBase = (function () {
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
); }) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836e..4447ddf2d 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -30,7 +30,7 @@ import { formDataChildren, FormDataPropertyView } from "../formComp/formDataCons import { withMethodExposing, refMethods } from "../../generators/withMethodExposing"; import { RefControl } from "../../controls/refControl"; import { styleControl } from "comps/controls/styleControl"; -import { InputLikeStyle, InputLikeStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { disabledPropertyView, hiddenPropertyView, @@ -256,6 +256,7 @@ const childrenMap = { onEvent: InputEventHandlerControl, viewRef: RefControl, style: styleControl(InputLikeStyle), + labelStyle:styleControl(LabelStyle), prefixIcon: IconControl, // validation @@ -377,7 +378,7 @@ const NumberInputTmpComp = (function () { return props.label({ required: props.required, children: , - style: props.style, + style: props.labelStyle, ...validate(props), }); }) @@ -425,9 +426,14 @@ const NumberInputTmpComp = (function () { )} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( + <>
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
+ )} )) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx index bea60397c..a3ff568fe 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx @@ -14,7 +14,7 @@ const RangeSliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx index b86b07ef6..4279588a8 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx @@ -18,7 +18,7 @@ const SliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx index 5e7760acb..601a72c0d 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx @@ -5,7 +5,7 @@ import { ChangeEventHandlerControl } from "../../controls/eventHandlerControl"; import { Section, sectionNames } from "lowcoder-design"; import { RecordConstructorToComp } from "lowcoder-core"; import { styleControl } from "comps/controls/styleControl"; -import { SliderStyle, SliderStyleType } from "comps/controls/styleControlConstants"; +import { LabelStyle, SliderStyle, SliderStyleType } from "comps/controls/styleControlConstants"; import styled, { css } from "styled-components"; import { default as Slider } from "antd/es/slider"; import { darkenColor, fadeColor } from "lowcoder-design"; @@ -67,6 +67,7 @@ export const SliderChildren = { disabled: BoolCodeControl, onEvent: ChangeEventHandlerControl, style: styleControl(SliderStyle), + labelStyle:styleControl(LabelStyle.filter((style)=> ['accent','validate'].includes(style.name) === false)), prefixIcon: IconControl, suffixIcon: IconControl, }; @@ -96,6 +97,9 @@ export const SliderPropertyView = (
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
)} diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx index e89c0e49f..10bc5d5b0 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx @@ -20,7 +20,7 @@ let CascaderBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( ['accent', 'validate'].includes(style.name) === false)), showSearch: BoolControl.DEFAULT_TRUE, viewRef: RefControl, - margin: MarginControl, + margin: MarginControl, padding: PaddingControl, }; @@ -71,9 +72,14 @@ export const CascaderPropertyView = ( )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index f8b154e42..c3e20a636 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -15,7 +15,7 @@ import { } from "./selectInputConstants"; import { formDataChildren } from "../formComp/formDataConstants"; import { styleControl } from "comps/controls/styleControl"; -import { CheckboxStyle, CheckboxStyleType } from "comps/controls/styleControlConstants"; +import { CheckboxStyle, CheckboxStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { RadioLayoutOptions, RadioPropertyView } from "./radioCompConstants"; import { dropdownControl } from "../../controls/dropdownControl"; import { ValueFromOption } from "lowcoder-design"; @@ -135,6 +135,7 @@ const CheckboxBasicComp = (function () { onEvent: ChangeEventHandlerControl, options: SelectInputOptionControl, style: styleControl(CheckboxStyle), + labelStyle:styleControl(LabelStyle), layout: dropdownControl(RadioLayoutOptions, "horizontal"), viewRef: RefControl, @@ -148,7 +149,7 @@ const CheckboxBasicComp = (function () { ] = useSelectInputValidate(props); return props.label({ required: props.required, - style: props.style, + style: props.labelStyle, children: ( , @@ -92,7 +93,10 @@ export const RadioPropertyView = ( )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + <>
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index 1a30f2522..644c3c958 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -1,5 +1,5 @@ import { styleControl } from "comps/controls/styleControl"; -import { SelectStyle } from "comps/controls/styleControlConstants"; +import { LabelStyle, SelectStyle } from "comps/controls/styleControlConstants"; import { trans } from "i18n"; import { stringExposingStateControl } from "../../controls/codeStateControl"; import { UICompBuilder } from "../../generators"; @@ -24,6 +24,7 @@ const SelectBasicComp = (function () { defaultValue: stringExposingStateControl("defaultValue"), value: stringExposingStateControl("value"), style: styleControl(SelectStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [ @@ -35,10 +36,10 @@ const SelectBasicComp = (function () { propsRef.current = props; const valueSet = new Set(props.options.map((o) => o.value)); // Filter illegal default values entered by the user - + return props.label({ required: props.required, - style: props.style, + style: props.labelStyle, children: ( ControlNode }; value: { propertyView: (params: ControlParams) => ControlNode }; style: { getPropertyView: () => ControlNode }; + labelStyle: { getPropertyView: () => ControlNode }; } ) => ( <> @@ -328,10 +329,15 @@ export const SelectPropertyView = ( {["layout", "both"].includes( useContext(EditorContext).editorModeStatus ) && ( -
- {children.style.getPropertyView()} -
- )} + <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ + )} ); diff --git a/client/packages/lowcoder/src/comps/comps/switchComp.tsx b/client/packages/lowcoder/src/comps/comps/switchComp.tsx index 3a3ecf955..03e164072 100644 --- a/client/packages/lowcoder/src/comps/comps/switchComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/switchComp.tsx @@ -4,7 +4,7 @@ import { booleanExposingStateControl } from "comps/controls/codeStateControl"; import { changeEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; import { LabelControl } from "comps/controls/labelControl"; import { styleControl } from "comps/controls/styleControl"; -import { SwitchStyle, SwitchStyleType } from "comps/controls/styleControlConstants"; +import { SwitchStyle, SwitchStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { Section, sectionNames } from "lowcoder-design"; import styled, { css } from "styled-components"; @@ -90,13 +90,14 @@ let SwitchTmpComp = (function () { onEvent: eventHandlerControl(EventOptions), disabled: BoolCodeControl, style: migrateOldData(styleControl(SwitchStyle), fixOldData), + labelStyle: styleControl(LabelStyle), viewRef: RefControl, ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( - {children.style.getPropertyView()} - + <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index b5c3d701d..ff0ab797a 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -25,7 +25,7 @@ import { import { withMethodExposing } from "../../generators/withMethodExposing"; import { styleControl } from "comps/controls/styleControl"; import styled from "styled-components"; -import { InputLikeStyle, InputLikeStyleType } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, minLengthPropertyView, @@ -41,7 +41,7 @@ import { RefControl } from "comps/controls/refControl"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; -const PasswordStyle = styled(InputPassword)<{ +const PasswordStyle = styled(InputPassword) <{ $style: InputLikeStyleType; }>` ${(props) => props.$style && getStyle(props.$style)} @@ -56,6 +56,7 @@ const PasswordTmpComp = (function () { visibilityToggle: BoolControl.DEFAULT_TRUE, prefixIcon: IconControl, style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); @@ -70,7 +71,7 @@ const PasswordTmpComp = (function () { $style={props.style} /> ), - style: props.style, + style: props.labelStyle, ...validateState, }); }) @@ -86,24 +87,27 @@ const PasswordTmpComp = (function () { {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <> -
{hiddenPropertyView(children)}
-
- {children.visibilityToggle.propertyView({ - label: trans("password.visibilityToggle"), - })} - {readOnlyPropertyView(children)} - {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} -
+
{hiddenPropertyView(children)}
+
+ {children.visibilityToggle.propertyView({ + label: trans("password.visibilityToggle"), + })} + {readOnlyPropertyView(children)} + {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} +
{requiredPropertyView(children)} {regexPropertyView(children)} {minLengthPropertyView(children)} {maxLengthPropertyView(children)} {children.customRule.propertyView({})} -
+
)} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( - <>
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index e11027a69..a7a155e84 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -21,7 +21,7 @@ import { import { withMethodExposing, refMethods } from "../../generators/withMethodExposing"; import { styleControl } from "comps/controls/styleControl"; import styled from "styled-components"; -import { InputLikeStyle, InputLikeStyleType } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { TextArea } from "components/TextArea"; import { allowClearPropertyView, @@ -36,7 +36,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; -const TextAreaStyled = styled(TextArea)<{ +const TextAreaStyled = styled(TextArea) <{ $style: InputLikeStyleType; }>` ${(props) => props.$style && getStyle(props.$style)} @@ -70,6 +70,7 @@ let TextAreaTmpComp = (function () { allowClear: BoolControl, autoHeight: withDefault(AutoHeightControl, "fixed"), style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); @@ -77,7 +78,7 @@ let TextAreaTmpComp = (function () { required: props.required, children: ( - ), - style: props.style, + style: props.labelStyle, ...validateState, }); }) @@ -101,19 +102,22 @@ let TextAreaTmpComp = (function () { {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <> -
- {children.autoHeight.getPropertyView()} - {hiddenPropertyView(children)} -
-
- {allowClearPropertyView(children)} - {readOnlyPropertyView(children)} -
- +
+ {children.autoHeight.getPropertyView()} + {hiddenPropertyView(children)} +
+
+ {allowClearPropertyView(children)} + {readOnlyPropertyView(children)} +
+ )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( - <>
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} )) diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index 6474efa9b..06b3b9040 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from "react"; import styled from "styled-components"; import ReactResizeDetector from "react-resize-detector"; import { StyleConfigType, styleControl } from "comps/controls/styleControl"; -import { TreeStyle } from "comps/controls/styleControlConstants"; +import { LabelStyle, TreeStyle } from "comps/controls/styleControlConstants"; import { LabelControl } from "comps/controls/labelControl"; import { withDefault } from "comps/generators"; import { dropdownControl } from "comps/controls/dropdownControl"; @@ -77,10 +77,11 @@ const childrenMap = { // TODO: more event onEvent: SelectEventHandlerControl, style: styleControl(TreeStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)) }; const TreeCompView = (props: RecordConstructorToView) => { - const { treeData, selectType, value, expanded, checkStrictly, style } = props; + const { treeData, selectType, value, expanded, checkStrictly, style, labelStyle } = props; const [height, setHeight] = useState(); const selectable = selectType === "single" || selectType === "multi"; const checkable = selectType === "check"; @@ -95,7 +96,7 @@ const TreeCompView = (props: RecordConstructorToView) => { return props.label({ required: props.required, ...selectInputValidate(props), - style: style, + style: labelStyle, children: ( setHeight(h)}> @@ -166,7 +167,7 @@ let TreeBasicComp = (function () { )} - + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (
{children.expanded.propertyView({ label: trans("tree.expanded") })} @@ -176,10 +177,13 @@ let TreeBasicComp = (function () {
)} - {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( children.label.getPropertyView() )} + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (children.label.getPropertyView())} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} )) diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx index ea31712a3..7db2d9e4c 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx @@ -6,7 +6,7 @@ import { default as TreeSelect } from "antd/es/tree-select"; import { useEffect } from "react"; import styled from "styled-components"; import { styleControl } from "comps/controls/styleControl"; -import { TreeSelectStyle, TreeSelectStyleType } from "comps/controls/styleControlConstants"; +import { LabelStyle, TreeSelectStyle, TreeSelectStyleType } from "comps/controls/styleControlConstants"; import { LabelControl } from "comps/controls/labelControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { @@ -66,6 +66,7 @@ const childrenMap = { showSearch: BoolControl.DEFAULT_TRUE, inputValue: stateComp(""), // search value style: styleControl(TreeSelectStyle), + labelStyle:styleControl(LabelStyle), viewRef: RefControl, }; @@ -83,7 +84,7 @@ function getCheckedStrategy(v: ValueFromOption) { const TreeCompView = ( props: RecordConstructorToView & { dispatch: DispatchType } ) => { - const { treeData, selectType, value, expanded, style, inputValue } = props; + const { treeData, selectType, value, expanded, style,labelStyle, inputValue } = props; const isSingle = selectType === "single"; const [ validateState, @@ -99,7 +100,7 @@ const TreeCompView = ( return props.label({ required: props.required, ...validateState, - style: style, + style: labelStyle, children: (
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} - - - )) .setExposeMethodConfigs(baseSelectRefMethods) diff --git a/client/packages/lowcoder/src/comps/controls/labelControl.tsx b/client/packages/lowcoder/src/comps/controls/labelControl.tsx index a2fe0947e..48233b318 100644 --- a/client/packages/lowcoder/src/comps/controls/labelControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/labelControl.tsx @@ -76,17 +76,16 @@ const LabelWrapper = styled.div<{ flex-shrink: 0; `; // ${(props) => props.$border && UnderlineCss}; -const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType }>` +const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType, $validateStatus: "success" | "warning" | "error" | "validating" | null }>` ${labelCss}; - font-family:${(props) => props.$labelStyle.fontFamily}; font-weight:${(props) => props.$labelStyle.fontWeight}; font-style:${(props) => props.$labelStyle.fontStyle}; text-transform:${(props) => props.$labelStyle.textTransform}; text-decoration:${(props) => props.$labelStyle.textDecoration}; font-size:${(props) => props.$labelStyle.textSize}; - color:${(props) => props.$labelStyle.text}; - ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${props.$labelStyle.border};`} + color:${(props) => !!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.text} !important; + ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} border-radius:${(props) => props.$labelStyle.radius}; padding:${(props) => props.$labelStyle.padding}; width: fit-content; @@ -194,6 +193,7 @@ export const LabelControl = (function () { > diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 4d6d09d0e..3ca944330 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -728,7 +728,7 @@ export const InputLikeStyle = [ ] as const; export const LabelStyle = [ - ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius') + ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius' && style.name !== 'background') ] export const RatingStyle = [ @@ -1153,11 +1153,11 @@ export const ImageStyle = [getStaticBorder("#00000000"), RADIUS, BORDER_WIDTH, M export const IconStyle = [ getStaticBackground("#00000000"), - getStaticBorder("#00000000"), + getStaticBorder("#00000000"), FILL, RADIUS, BORDER_WIDTH, - MARGIN, + MARGIN, PADDING] as const; From 15ee23458c7900f37b9e7c27e1d463551a7ef3c8 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Mon, 4 Mar 2024 20:35:37 +0500 Subject: [PATCH 14/33] Rating component, signature component ,date component label styling panel and properties added along with default values of latest add CSS properties --- .../src/comps/comps/dateComp/dateComp.tsx | 65 +++++--- .../lowcoder/src/comps/comps/ratingComp.tsx | 32 ++-- .../src/comps/comps/signatureComp.tsx | 23 ++- .../src/comps/controls/styleControl.tsx | 154 +++++++++++------- .../comps/controls/styleControlConstants.tsx | 12 +- 5 files changed, 172 insertions(+), 114 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index ceeb9a257..2d5030132 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -20,7 +20,7 @@ import { UICompBuilder, withDefault } from "../../generators"; import { CommonNameConfig, depsConfig, withExposingConfigs } from "../../generators/withExposing"; import { formDataChildren, FormDataPropertyView } from "../formComp/formDataConstants"; import { styleControl } from "comps/controls/styleControl"; -import { DateTimeStyle, DateTimeStyleType } from "comps/controls/styleControlConstants"; +import { DateTimeStyle, DateTimeStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { withMethodExposing } from "../../generators/withMethodExposing"; import { disabledPropertyView, @@ -72,6 +72,7 @@ const commonChildren = { minuteStep: RangeControl.closed(1, 60, 1), secondStep: RangeControl.closed(1, 60, 1), style: styleControl(DateTimeStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), suffixIcon: withDefault(IconControl, "/icon:regular/calendar"), ...validationChildren, viewRef: RefControl, @@ -159,12 +160,12 @@ export type DateCompViewProps = Pick< export const datePickerControl = new UICompBuilder(childrenMap, (props) => { let time = dayjs(null); - if(props.value.value !== '') { + if (props.value.value !== '') { time = dayjs(props.value.value, DateParser); } return props.label({ required: props.required, - style: props.style, + style: props.labelStyle, children: ( { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
- {requiredPropertyView(children)} - {dateValidationFields(children)} - {timeValidationFields(children)} - {children.customRule.propertyView({})} -
+ {requiredPropertyView(children)} + {dateValidationFields(children)} + {timeValidationFields(children)} + {children.customRule.propertyView({})} +
{children.onEvent.getPropertyView()} {disabledPropertyView(children)} @@ -234,9 +235,9 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { {children.placeholder.propertyView({ label: trans("date.placeholderText") })}
)} - + {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( - <>
+ <>
{timeFields(children, isMobile)} {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })}
@@ -244,9 +245,14 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && !isMobile && commonAdvanceSection(children)} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); @@ -264,10 +270,10 @@ export const dateRangeControl = (function () { return new UICompBuilder(childrenMap, (props) => { let start = dayjs(null); let end = dayjs(null); - if(props.start.value !== '') { + if (props.start.value !== '') { start = dayjs(props.start.value, DateParser); } - if(props.end.value !== '') { + if (props.end.value !== '') { end = dayjs(props.end.value, DateParser); } @@ -309,13 +315,13 @@ export const dateRangeControl = (function () { return props.label({ required: props.required, - style: props.style, + style: props.labelStyle, children: children, ...(startResult.validateStatus !== "success" ? startResult : endResult.validateStatus !== "success" - ? endResult - : startResult), + ? endResult + : startResult), }); }) .setPropertyViewFn((children) => { @@ -337,11 +343,11 @@ export const dateRangeControl = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
- {requiredPropertyView(children)} - {dateValidationFields(children)} - {timeValidationFields(children)} - {children.customRule.propertyView({})} -
+ {requiredPropertyView(children)} + {dateValidationFields(children)} + {timeValidationFields(children)} + {children.customRule.propertyView({})} +
{children.onEvent.getPropertyView()} {disabledPropertyView(children)} @@ -358,7 +364,7 @@ export const dateRangeControl = (function () { {children.placeholder.propertyView({ label: trans("date.placeholderText") })}
)} - + {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{timeFields(children, isMobile)} @@ -368,9 +374,14 @@ export const dateRangeControl = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && commonAdvanceSection(children)} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} diff --git a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx index 40db580ab..a0504d081 100644 --- a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx @@ -10,7 +10,7 @@ import { UICompBuilder, withDefault } from "../generators"; import { CommonNameConfig, NameConfig, withExposingConfigs } from "../generators/withExposing"; import { formDataChildren, FormDataPropertyView } from "./formComp/formDataConstants"; import { styleControl } from "comps/controls/styleControl"; -import { RatingStyle, RatingStyleType } from "comps/controls/styleControlConstants"; +import { LabelStyle, RatingStyle, RatingStyleType } from "comps/controls/styleControlConstants"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; @@ -44,6 +44,7 @@ const RatingBasicComp = (function () { disabled: BoolCodeControl, onEvent: eventHandlerControl(EventOptions), style: migrateOldData(styleControl(RatingStyle), fixOldData), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { @@ -63,7 +64,7 @@ const RatingBasicComp = (function () { }, [value]); return props.label({ - style: props.style, + style: props.labelStyle, children: (
- {children.onEvent.getPropertyView()} - {disabledPropertyView(children)} - {hiddenPropertyView(children)} -
+ {children.onEvent.getPropertyView()} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
- {children.allowHalf.propertyView({ - label: trans("rating.allowHalf"), - })} + {children.allowHalf.propertyView({ + label: trans("rating.allowHalf"), + })}
)} @@ -110,9 +111,14 @@ const RatingBasicComp = (function () { )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); @@ -144,6 +150,6 @@ const getStyle = (style: RatingStyleType) => { `; }; -export const RateStyled = styled(Rate)<{ $style: RatingStyleType }>` +export const RateStyled = styled(Rate) <{ $style: RatingStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; diff --git a/client/packages/lowcoder/src/comps/comps/signatureComp.tsx b/client/packages/lowcoder/src/comps/comps/signatureComp.tsx index 8db397915..0f16be8df 100644 --- a/client/packages/lowcoder/src/comps/comps/signatureComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/signatureComp.tsx @@ -8,6 +8,7 @@ import { styleControl } from "comps/controls/styleControl"; import { contrastColor, SignatureStyle, + LabelStyle, SignatureStyleType, widthCalculator, heightCalculator @@ -38,11 +39,11 @@ const Wrapper = styled.div<{ $style: SignatureStyleType; $isEmpty: boolean }>` overflow: hidden; width: 100%; height: 100%; - width: ${(props) => { - return widthCalculator(props.$style.margin); + width: ${(props) => { + return widthCalculator(props.$style.margin); }}; - height: ${(props) => { - return heightCalculator(props.$style.margin); + height: ${(props) => { + return heightCalculator(props.$style.margin); }}; margin: ${(props) => props.$style.margin}; padding: ${(props) => props.$style.padding}; @@ -98,6 +99,7 @@ const childrenMap = { onEvent: ChangeEventHandlerControl, label: withDefault(LabelControl, { position: "column", text: "" }), style: styleControl(SignatureStyle), + labelStyle: styleControl(LabelStyle), showUndo: withDefault(BoolControl, true), showClear: withDefault(BoolControl, true), value: stateComp(""), @@ -125,7 +127,7 @@ let SignatureTmpComp = (function () { } }; return props.label({ - style: props.style, + style: props.labelStyle, children: ( { @@ -218,9 +220,14 @@ let SignatureTmpComp = (function () { )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index e9ad4b6d9..880b1a69d 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -53,6 +53,9 @@ import { FooterBackgroundImageSizeConfig, FooterBackgroundImagePositionConfig, FooterBackgroundImageOriginConfig, + TextTransformConfig, + TextDecorationConfig, + borderStyleConfig, } from "./styleControlConstants"; @@ -140,6 +143,15 @@ function isFontFamilyConfig(config: SingleColorConfig): config is FontFamilyConf function isFontStyleConfig(config: SingleColorConfig): config is FontStyleConfig { return config.hasOwnProperty("fontStyle"); } +function isTextTransformConfig(config: SingleColorConfig): config is TextTransformConfig { + return config.hasOwnProperty("textTransform"); +} +function isTextDecorationConfig(config: SingleColorConfig): config is TextDecorationConfig { + return config.hasOwnProperty("textDecoration"); +} +function isBorderStyleConfig(config: SingleColorConfig): config is borderStyleConfig { + return config.hasOwnProperty("borderStyle"); +} function isMarginConfig(config: SingleColorConfig): config is MarginConfig { return config.hasOwnProperty("margin"); @@ -222,6 +234,15 @@ function isEmptyFontFamily(fontFamily: string) { function isEmptyFontStyle(fontStyle: string) { return _.isEmpty(fontStyle); } +function isEmptyTextTransform(textTransform: string) { + return _.isEmpty(textTransform); +} +function isEmptyTextDecoration(textDecoration: string) { + return _.isEmpty(textDecoration); +} +function isEmptyBorderStyle(borderStyle: string) { + return _.isEmpty(borderStyle); +} function isEmptyMargin(margin: string) { return _.isEmpty(margin); @@ -328,6 +349,18 @@ function calcColors>( res[name] = props[name]; return; } + if (!isEmptyTextTransform(props[name]) && isTextTransformConfig(config)) { + res[name] = props[name]; + return; + } + if (!isEmptyTextDecoration(props[name]) && isTextDecorationConfig(config)) { + res[name] = props[name]; + return; + } + if (!isEmptyBorderStyle(props[name]) && isBorderStyleConfig(config)) { + res[name] = props[name]; + return; + } if (!isEmptyMargin(props[name]) && isMarginConfig(config)) { res[name] = props[name]; return; @@ -412,6 +445,15 @@ function calcColors>( if (isFontStyleConfig(config)) { res[name] = themeWithDefault[config.fontStyle] || 'normal' } + if(isTextTransformConfig(config)){ + res[name] = themeWithDefault[config.textTransform] || 'none' + } + if(isTextDecorationConfig(config)){ + res[name] = themeWithDefault[config.textDecoration] || 'none' + } + if(isBorderStyleConfig(config)){ + res[name] = themeWithDefault[config.borderStyle] || 'dashed' + } if (isMarginConfig(config)) { res[name] = themeWithDefault[config.margin]; } @@ -689,126 +731,126 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], - }): name === "borderStyle" - ? ( - children[name] as InstanceType - ).propertyView({ - label: config.label, - preInputNode: , - placeholder: props[name], - }) - : name === "margin" + }) : name === "borderStyle" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : (name === "padding" || - name === "containerheaderpadding" || - name === "containerfooterpadding" || - name === "containerbodypadding") + : name === "margin" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "textSize" + : (name === "padding" || + name === "containerheaderpadding" || + name === "containerfooterpadding" || + name === "containerbodypadding") ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "textWeight" + : name === "textSize" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "fontFamily" + : name === "textWeight" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , - placeholder: props[name], - }): name === "textDecoration" - ? ( - children[name] as InstanceType - ).propertyView({ - label: config.label, - preInputNode: , - placeholder: props[name], - }): name === "textTransform" - ? ( - children[name] as InstanceType - ).propertyView({ - label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "fontStyle" + : name === "fontFamily" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], - }) - : name === "backgroundImage" || name === "headerBackgroundImage" || name === "footerBackgroundImage" + }) : name === "textDecoration" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], - }) - : name === "backgroundImageRepeat" || name === "headerBackgroundImageRepeat" || name === "footerBackgroundImageRepeat" + }) : name === "textTransform" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "backgroundImageSize" || name === "headerBackgroundImageSize" || name === "footerBackgroundImageSize" + : name === "fontStyle" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "backgroundImagePosition" || name === "headerBackgroundImagePosition" || name === "footerBackgroundImagePosition" + : name === "backgroundImage" || name === "headerBackgroundImage" || name === "footerBackgroundImage" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "backgroundImageOrigin" || name === "headerBackgroundImageOrigin" || name === "footerBackgroundImageOrigin" + : name === "backgroundImageRepeat" || name === "headerBackgroundImageRepeat" || name === "footerBackgroundImageRepeat" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : children[name].propertyView({ - label: config.label, - panelDefaultColor: props[name], - // isDep: isDepColorConfig(config), - isDep: true, - depMsg: depMsg, - })} + : name === "backgroundImageSize" || name === "headerBackgroundImageSize" || name === "footerBackgroundImageSize" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }) + : name === "backgroundImagePosition" || name === "headerBackgroundImagePosition" || name === "footerBackgroundImagePosition" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }) + : name === "backgroundImageOrigin" || name === "headerBackgroundImageOrigin" || name === "footerBackgroundImageOrigin" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }) + : children[name].propertyView({ + label: config.label, + panelDefaultColor: props[name], + // isDep: isDepColorConfig(config), + isDep: true, + depMsg: depMsg, + })} ); })} diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 3ca944330..5aeec739c 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -701,7 +701,6 @@ export const ContainerFooterStyle = [ ] as const; export const SliderStyle = [ - LABEL, FILL, { name: "thumbBoder", @@ -721,7 +720,6 @@ export const SliderStyle = [ ] as const; export const InputLikeStyle = [ - // LABEL, getStaticBackground(SURFACE_COLOR), ...STYLING_FIELDS_SEQUENCE, ...ACCENT_VALIDATE, @@ -732,7 +730,6 @@ export const LabelStyle = [ ] export const RatingStyle = [ - LABEL, { name: "checked", label: trans("style.checked"), @@ -748,7 +745,6 @@ export const RatingStyle = [ ] as const; export const SwitchStyle = [ - LABEL, { name: "handle", label: trans("style.handle"), @@ -838,7 +834,6 @@ export const ModalStyle = [ ] as const; export const CascaderStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc"), TEXT, ACCENT, @@ -870,7 +865,7 @@ function checkAndUncheck() { } export const CheckboxStyle = [ - ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border'), + ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border'), ...checkAndUncheck(), { name: "checked", @@ -883,7 +878,7 @@ export const CheckboxStyle = [ ] as const; export const RadioStyle = [ - ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border' && style.name !== 'radius'), + ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border' && style.name !== 'radius'), ...checkAndUncheck(), { name: "checked", @@ -1055,7 +1050,6 @@ export const FileViewerStyle = [ export const IframeStyle = [getBackground(), getStaticBorder("#00000000"), RADIUS, BORDER_WIDTH, MARGIN, PADDING] as const; export const DateTimeStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), TEXT, MARGIN, @@ -1202,7 +1196,6 @@ export const TimeLineStyle = [ ] as const; export const TreeStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), TEXT, VALIDATE, @@ -1259,7 +1252,6 @@ export const CalendarStyle = [ ] as const; export const SignatureStyle = [ - LABEL, ...getBgBorderRadiusByBg(), { name: "pen", From 80d78f946b93dac7368ada49a8a6dbd143615d0f Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 5 Mar 2024 15:00:34 +0500 Subject: [PATCH 15/33] separate defaultValue and value for old input/select comps --- .../comps/numberInputComp/numberInputComp.tsx | 6 +++++- .../comps/comps/selectInputComp/checkboxComp.tsx | 6 +++++- .../comps/selectInputComp/multiSelectComp.tsx | 7 +++++-- .../src/comps/comps/selectInputComp/radioComp.tsx | 6 +++++- .../comps/selectInputComp/segmentedControl.tsx | 7 ++++++- .../src/comps/comps/selectInputComp/selectComp.tsx | 6 +++++- .../src/comps/comps/textInputComp/inputComp.tsx | 9 ++++++++- .../src/comps/comps/textInputComp/mentionComp.tsx | 7 ++++++- .../src/comps/comps/textInputComp/passwordComp.tsx | 6 +++++- .../src/comps/comps/textInputComp/textAreaComp.tsx | 4 ++++ .../comps/textInputComp/textInputConstants.tsx | 14 ++++++++++++++ 11 files changed, 68 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836e..58dcfe8c4 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -52,6 +52,8 @@ import { import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; const getStyle = (style: InputLikeStyleType) => { return css` @@ -372,7 +374,7 @@ const CustomInputNumber = (props: RecordConstructorToView) = ); }; -const NumberInputTmpComp = (function () { +let NumberInputTmpComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ required: props.required, @@ -434,6 +436,8 @@ const NumberInputTmpComp = (function () { .build(); })(); +NumberInputTmpComp = migrateOldData(NumberInputTmpComp, fixOldInputCompData); + const NumberInputTmp2Comp = withMethodExposing( NumberInputTmpComp, refMethods([ diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index f8b154e42..0cb4587a6 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -22,6 +22,8 @@ import { ValueFromOption } from "lowcoder-design"; import { EllipsisTextCss } from "lowcoder-design"; import { trans } from "i18n"; import { RefControl } from "comps/controls/refControl"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; export const getStyle = (style: CheckboxStyleType) => { return css` @@ -126,7 +128,7 @@ const CheckboxGroup = styled(AntdCheckboxGroup) <{ }} `; -const CheckboxBasicComp = (function () { +let CheckboxBasicComp = (function () { const childrenMap = { defaultValue: arrayStringExposingStateControl("defaultValue"), value: arrayStringExposingStateControl("value"), @@ -176,6 +178,8 @@ const CheckboxBasicComp = (function () { .build(); })(); +CheckboxBasicComp = migrateOldData(CheckboxBasicComp, fixOldInputCompData); + export const CheckboxComp = withExposingConfigs(CheckboxBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx index a8c2c0dc1..c45c0cdc6 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx @@ -14,9 +14,10 @@ import { SelectInputInvalidConfig, useSelectInputValidate } from "./selectInputC import { PaddingControl } from "../../controls/paddingControl"; import { MarginControl } from "../../controls/marginControl"; -import { useEffect, useRef } from "react"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; -const MultiSelectBasicComp = (function () { +let MultiSelectBasicComp = (function () { const childrenMap = { ...SelectChildrenMap, defaultValue: arrayStringExposingStateControl("defaultValue", ["1", "2"]), @@ -52,6 +53,8 @@ const MultiSelectBasicComp = (function () { .build(); })(); +MultiSelectBasicComp = migrateOldData(MultiSelectBasicComp, fixOldInputCompData); + export const MultiSelectComp = withExposingConfigs(MultiSelectBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), new NameConfig("inputValue", trans("select.inputValueDesc")), diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx index 11bfceed0..4ab4add86 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx @@ -11,6 +11,8 @@ import { } from "./selectInputConstants"; import { EllipsisTextCss, ValueFromOption } from "lowcoder-design"; import { trans } from "i18n"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const getStyle = (style: RadioStyleType) => { return css` @@ -93,7 +95,7 @@ const Radio = styled(AntdRadioGroup)<{ }} `; -const RadioBasicComp = (function () { +let RadioBasicComp = (function () { return new UICompBuilder(RadioChildrenMap, (props) => { const [ validateState, @@ -129,6 +131,8 @@ const RadioBasicComp = (function () { .build(); })(); +RadioBasicComp = migrateOldData(RadioBasicComp, fixOldInputCompData); + export const RadioComp = withExposingConfigs(RadioBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index 73a7d4675..a73827c2a 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx @@ -25,6 +25,9 @@ import { RefControl } from "comps/controls/refControl"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; + const getStyle = (style: SegmentStyleType) => { return css` @@ -83,7 +86,7 @@ const SegmentChildrenMap = { ...formDataChildren, }; -const SegmentedControlBasicComp = (function () { +let SegmentedControlBasicComp = (function () { return new UICompBuilder(SegmentChildrenMap, (props) => { const [ validateState, @@ -147,6 +150,8 @@ const SegmentedControlBasicComp = (function () { .build(); })(); +SegmentedControlBasicComp = migrateOldData(SegmentedControlBasicComp, fixOldInputCompData); + export const SegmentedControlComp = withExposingConfigs(SegmentedControlBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index 1a30f2522..50455b335 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -17,8 +17,10 @@ import { } from "./selectInputConstants"; import { useRef } from "react"; import { RecordConstructorToView } from "lowcoder-core"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { migrateOldData } from "comps/generators/simpleGenerators"; -const SelectBasicComp = (function () { +let SelectBasicComp = (function () { const childrenMap = { ...SelectChildrenMap, defaultValue: stringExposingStateControl("defaultValue"), @@ -55,6 +57,8 @@ const SelectBasicComp = (function () { .build(); })(); +SelectBasicComp = migrateOldData(SelectBasicComp, fixOldInputCompData); + export const SelectComp = withExposingConfigs(SelectBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), new NameConfig("inputValue", trans("select.inputValueDesc")), diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx index fc34bc723..5eacf07cf 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx @@ -11,6 +11,7 @@ import styled from "styled-components"; import { UICompBuilder } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, inputRefMethods, TextInputBasicSection, @@ -30,6 +31,7 @@ import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; import { InputRef } from "antd/es/input"; import { RefControl } from "comps/controls/refControl"; +import { migrateOldData } from "comps/generators/simpleGenerators"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; @@ -52,7 +54,7 @@ const childrenMap = { suffixIcon: IconControl, }; -export const InputComp = new UICompBuilder(childrenMap, (props) => { +let InputBasicComp = new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); return props.label({ required: props.required, @@ -108,3 +110,8 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { ...TextInputConfigs, ]) .build(); + + +const InputComp = migrateOldData(InputBasicComp, fixOldInputCompData); + +export { InputComp }; diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx index 51815260f..9bad13d1e 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -12,6 +12,7 @@ import { UICompBuilder } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { checkMentionListData, + fixOldInputCompData, textInputChildren, } from "./textInputConstants"; import { @@ -42,7 +43,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import { textInputValidate, } from "../textInputComp/textInputConstants"; -import { jsonControl } from "@lowcoder-ee/comps/controls/codeControl"; +import { jsonControl } from "comps/controls/codeControl"; import { submitEvent, eventHandlerControl, @@ -54,6 +55,7 @@ import { import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const Wrapper = styled.div<{ $style: InputLikeStyleType; @@ -267,12 +269,15 @@ let MentionTmpComp = (function () { .build(); })(); + MentionTmpComp = class extends MentionTmpComp { override autoHeight(): boolean { return this.children.autoHeight.getView(); } }; +MentionTmpComp = migrateOldData(MentionTmpComp, fixOldInputCompData); + const TextareaTmp2Comp = withMethodExposing( MentionTmpComp, refMethods([focusWithOptions, blurMethod]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index b5c3d701d..7659cdf72 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -13,6 +13,7 @@ import { LabelControl } from "../../controls/labelControl"; import { UICompBuilder, withDefault } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, inputRefMethods, TextInputBasicSection, @@ -40,6 +41,7 @@ import { hasIcon } from "comps/utils"; import { RefControl } from "comps/controls/refControl"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const PasswordStyle = styled(InputPassword)<{ $style: InputLikeStyleType; @@ -47,7 +49,7 @@ const PasswordStyle = styled(InputPassword)<{ ${(props) => props.$style && getStyle(props.$style)} `; -const PasswordTmpComp = (function () { +let PasswordTmpComp = (function () { const childrenMap = { ...textInputChildren, viewRef: RefControl, @@ -111,6 +113,8 @@ const PasswordTmpComp = (function () { .build(); })(); +PasswordTmpComp = migrateOldData(PasswordTmpComp, fixOldInputCompData); + const PasswordTmp2Comp = withMethodExposing(PasswordTmpComp, inputRefMethods); export const PasswordComp = withExposingConfigs(PasswordTmp2Comp, [ diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index e11027a69..fe6a4ad24 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -10,6 +10,7 @@ import { AutoHeightControl } from "../../controls/autoHeightControl"; import { UICompBuilder, withDefault } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, TextInputBasicSection, textInputChildren, @@ -35,6 +36,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const TextAreaStyled = styled(TextArea)<{ $style: InputLikeStyleType; @@ -126,6 +128,8 @@ TextAreaTmpComp = class extends TextAreaTmpComp { } }; +TextAreaTmpComp = migrateOldData(TextAreaTmpComp, fixOldInputCompData); + const TextareaTmp2Comp = withMethodExposing( TextAreaTmpComp, refMethods([focusWithOptions, blurMethod]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 1d01266af..9c9d17cbb 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -305,3 +305,17 @@ export function checkMentionListData(data: any) { } return data } + +// separate defaultValue and value for old components +export function fixOldInputCompData(oldData: any) { + if (!oldData) return oldData; + if (Boolean(oldData.value) && !Boolean(oldData.defaultValue)) { + const value = oldData.value; + return { + ...oldData, + defaultValue: value, + value: '', + }; + } + return oldData; +} From f324f750e56a3b4c7eb69a335aa4955500bf1a66 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 5 Mar 2024 18:20:58 +0500 Subject: [PATCH 16/33] map ready state fix --- .../packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx b/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx index b6668f1c9..092a8d382 100644 --- a/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx @@ -120,7 +120,9 @@ ChartTmpComp = withViewFn(ChartTmpComp, (comp) => { const handleOnMapScriptLoad = () => { setMapScriptLoaded(true); - loadGoogleMapData(); + setTimeout(() => { + loadGoogleMapData(); + }) } useEffect(() => { From abe4b2049a10f860fae4985f7b7f34dbd5939aa0 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 5 Mar 2024 18:22:16 +0500 Subject: [PATCH 17/33] update lowcoder-comps version --- client/packages/lowcoder-comps/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 01733833e..029be11e2 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "0.0.26", + "version": "0.0.27", "type": "module", "license": "MIT", "dependencies": { From e9a17d7735b01529065e948e679362385e3a88dd Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Thu, 7 Mar 2024 17:33:27 +0500 Subject: [PATCH 18/33] Fixed issue when apply bulk margin and padding --- .../lowcoder-design/src/components/Label.tsx | 1 - .../comps/autoCompleteComp/autoCompleteComp.tsx | 3 ++- .../lowcoder/src/comps/comps/dateComp/dateComp.tsx | 6 ++++-- .../comps/comps/numberInputComp/numberInputComp.tsx | 3 ++- .../comps/comps/numberInputComp/rangeSliderComp.tsx | 5 +++-- .../src/comps/comps/numberInputComp/sliderComp.tsx | 3 ++- .../packages/lowcoder/src/comps/comps/ratingComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/cascaderComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/checkboxComp.tsx | 9 +++++---- .../comps/comps/selectInputComp/multiSelectComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/radioComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/selectComp.tsx | 3 ++- .../lowcoder/src/comps/comps/signatureComp.tsx | 3 ++- .../packages/lowcoder/src/comps/comps/switchComp.tsx | 5 +++-- .../src/comps/comps/textInputComp/inputComp.tsx | 5 +++-- .../src/comps/comps/textInputComp/passwordComp.tsx | 3 ++- .../src/comps/comps/textInputComp/textAreaComp.tsx | 5 +++-- .../comps/comps/textInputComp/textInputConstants.tsx | 2 +- .../lowcoder/src/comps/comps/treeComp/treeComp.tsx | 3 ++- .../src/comps/comps/treeComp/treeSelectComp.tsx | 3 ++- .../lowcoder/src/comps/controls/labelControl.tsx | 12 ++++++++---- 21 files changed, 54 insertions(+), 32 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/Label.tsx b/client/packages/lowcoder-design/src/components/Label.tsx index 0ec5cce40..91b846acb 100644 --- a/client/packages/lowcoder-design/src/components/Label.tsx +++ b/client/packages/lowcoder-design/src/components/Label.tsx @@ -5,7 +5,6 @@ export const labelCss: any = css` user-select: none; font-size: 13px; - color: #222222; &:hover { cursor: default; diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index d052bf4cb..ab1a992fa 100644 --- a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx @@ -278,7 +278,8 @@ let AutoCompleteCompBase = (function () { ), - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index 2d5030132..2977a60e2 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -165,7 +165,8 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { } return props.label({ required: props.required, - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( , - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, ...validate(props), }); }) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx index a3ff568fe..7deb69530 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx @@ -14,7 +14,8 @@ const RangeSliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, children: ( { @@ -28,7 +29,7 @@ const RangeSliderBasicComp = (function () { range={true} value={[props.start.value, props.end.value]} $style={props.style} - style={{margin: 0}} + style={{ margin: 0 }} onChange={([start, end]) => { props.start.onChange(start); props.end.onChange(end); diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx index 4279588a8..23181e5d8 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx @@ -18,7 +18,8 @@ const SliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx index a0504d081..df93e688c 100644 --- a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx @@ -64,7 +64,8 @@ const RatingBasicComp = (function () { }, [value]); return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, children: ( { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( { &:hover .ant-checkbox-inner, .ant-checkbox:hover .ant-checkbox-inner, .ant-checkbox-input + ant-checkbox-inner { - background-color:${style.hoverBackground ? style.hoverBackground :'#fff'}; + background-color:${style.hoverBackground ? style.hoverBackground : '#fff'}; } &:hover .ant-checkbox-checked .ant-checkbox-inner, .ant-checkbox:hover .ant-checkbox-inner, .ant-checkbox-input + ant-checkbox-inner { - background-color:${style.hoverBackground ? style.hoverBackground:'#ffff'}; + background-color:${style.hoverBackground ? style.hoverBackground : '#ffff'}; } &:hover .ant-checkbox-inner, @@ -135,7 +135,7 @@ const CheckboxBasicComp = (function () { onEvent: ChangeEventHandlerControl, options: SelectInputOptionControl, style: styleControl(CheckboxStyle), - labelStyle:styleControl(LabelStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), layout: dropdownControl(RadioLayoutOptions, "horizontal"), viewRef: RefControl, @@ -149,7 +149,8 @@ const CheckboxBasicComp = (function () { ] = useSelectInputValidate(props); return props.label({ required: props.required, - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/switchComp.tsx b/client/packages/lowcoder/src/comps/comps/switchComp.tsx index 03e164072..70c6a4379 100644 --- a/client/packages/lowcoder/src/comps/comps/switchComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/switchComp.tsx @@ -90,14 +90,15 @@ let SwitchTmpComp = (function () { onEvent: eventHandlerControl(EventOptions), disabled: BoolCodeControl, style: migrateOldData(styleControl(SwitchStyle), fixOldData), - labelStyle: styleControl(LabelStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), viewRef: RefControl, ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( ` +const InputStyle = styled(Input) <{ $style: InputLikeStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; @@ -68,7 +68,8 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { suffix={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index ff0ab797a..2a3a23f67 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -71,7 +71,8 @@ const PasswordTmpComp = (function () { $style={props.style} /> ), - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index a7a155e84..a47028235 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -46,7 +46,7 @@ const Wrapper = styled.div<{ $style: InputLikeStyleType; }>` height: 100% !important; - + .ant-input { height:100% !important; } @@ -87,7 +87,8 @@ let TextAreaTmpComp = (function () { /> ), - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 6f244ef2c..cfbe94c41 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -250,7 +250,7 @@ export function getStyle(style: InputLikeStyleType, labelStyle?: LabelStyleType) font-style:${style.fontStyle}; text-transform:${style.textTransform}; text-decoration:${style.textDecoration}; - background-color: ${style.background}; + // background-color: ${style.background}; border-color: ${style.border}; &:focus, diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index 06b3b9040..753df6688 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -96,7 +96,8 @@ const TreeCompView = (props: RecordConstructorToView) => { return props.label({ required: props.required, ...selectInputValidate(props), - style: labelStyle, + style, + labelStyle, children: ( setHeight(h)}> diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx index 7db2d9e4c..de15121b3 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx @@ -100,7 +100,8 @@ const TreeCompView = ( return props.label({ required: props.required, ...validateState, - style: labelStyle, + style, + labelStyle, children: ( & { children: ReactNode; style?: Record; + labelStyle?: Record; }; const StyledStarIcon = styled(StarIcon)` @@ -76,18 +77,21 @@ const LabelWrapper = styled.div<{ flex-shrink: 0; `; // ${(props) => props.$border && UnderlineCss}; +// ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} + const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType, $validateStatus: "success" | "warning" | "error" | "validating" | null }>` ${labelCss}; font-family:${(props) => props.$labelStyle.fontFamily}; - font-weight:${(props) => props.$labelStyle.fontWeight}; + font-weight:${(props) => props.$labelStyle.textWeight}; font-style:${(props) => props.$labelStyle.fontStyle}; text-transform:${(props) => props.$labelStyle.textTransform}; text-decoration:${(props) => props.$labelStyle.textDecoration}; font-size:${(props) => props.$labelStyle.textSize}; - color:${(props) => !!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.text} !important; - ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} + color:${(props) => !!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.label} !important; + ${(props) => `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} border-radius:${(props) => props.$labelStyle.radius}; padding:${(props) => props.$labelStyle.padding}; + margin:${(props) => props.$labelStyle.margin}; width: fit-content; user-select: text; white-space: nowrap; @@ -194,7 +198,7 @@ export const LabelControl = (function () { From eb0e0d5a07b459e3711c8cfebedbf8b4a5da27ae Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Wed, 6 Mar 2024 21:39:03 +0500 Subject: [PATCH 19/33] feature: Extended folder to have more meta-data i.e: title, description, category, type, image --- .../java/org/lowcoder/domain/folder/model/Folder.java | 5 +++++ .../java/org/lowcoder/api/home/FolderApiService.java | 9 +++++++++ .../main/java/org/lowcoder/api/home/FolderInfoView.java | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java index a254da0f1..88bc8b7da 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java @@ -17,4 +17,9 @@ public class Folder extends HasIdAndAuditing { @Nullable private String parentFolderId; // null represents folder in the root folder private String name; + private String title; + private String description; + private String category; + private String type; + private String image; } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java index 69e4517d5..d5fc1a4eb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -182,6 +182,11 @@ private Mono removePermissions(String folderId) { public Mono update(Folder folder) { Folder newFolder = new Folder(); newFolder.setName(folder.getName()); + newFolder.setTitle(folder.getTitle()); + newFolder.setType(folder.getType()); + newFolder.setCategory(folder.getCategory()); + newFolder.setDescription(folder.getDescription()); + newFolder.setImage(folder.getImage()); return checkManagePermission(folder.getId()) .then(folderService.updateById(folder.getId(), newFolder)) .then(folderService.findById(folder.getId())) @@ -421,6 +426,10 @@ public Mono buildFolderInfoView(Folder folder, boolean visible, .folderId(folder.getId()) .parentFolderId(folder.getParentFolderId()) .name(folder.getName()) + .description(folder.getDescription()) + .category(folder.getCategory()) + .type(folder.getType()) + .image(folder.getImage()) .createAt(folder.getCreatedAt() == null ? 0 : folder.getCreatedAt().toEpochMilli()) .createBy(user.getName()) .createTime(folder.getCreatedAt()) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java index b1abb505f..17776f298 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java @@ -20,6 +20,11 @@ public class FolderInfoView { private final String folderId; private final String parentFolderId; private final String name; + private final String title; + private final String description; + private final String category; + private final String type; + private final String image; private final Long createAt; private final String createBy; private boolean isVisible; From 488e2342d316e4dbb85d6fec5843a45a5ec98750 Mon Sep 17 00:00:00 2001 From: Ludovit Mikula Date: Mon, 5 Feb 2024 10:38:30 +0100 Subject: [PATCH 20/33] New plugin system * Add support for SUPER_ADMIN role * Publish server log events * Add handling for audit logs feature * Add handling for geolocation data * Add handling for api delays in case of rate limit * Propagate plugin specific environment variables to plugins * Add environment variable for controlling plugin location * Implemented plugin endpoints security --------- Co-authored-by: Abdul Qadir --- .gitignore | 1 + deploy/docker/Dockerfile | 20 +- deploy/docker/api-service/entrypoint.sh | 6 +- server/api-service/.gitignore | 6 +- server/api-service/PLUGIN.md | 122 ++++ server/api-service/distribution/pom.xml | 84 +++ .../distribution/src/assembly/bin.xml | 72 ++ .../src/assembly/set-classpath.sh | 11 + .../api-service/lowcoder-dependencies/pom.xml | 224 +++++++ server/api-service/lowcoder-domain/pom.xml | 36 +- .../configurations/Pf4jConfiguration.java | 15 - .../domain/group/model/GroupMember.java | 5 + .../domain/organization/model/MemberRole.java | 3 +- .../domain/organization/model/OrgMember.java | 4 + .../service/OrganizationService.java | 4 +- .../service/OrganizationServiceImpl.java | 18 +- .../service/ResourcePermissionHandler.java | 4 +- .../domain/user/service/UserServiceImpl.java | 2 +- server/api-service/lowcoder-infra/pom.xml | 19 + .../lowcoder/infra/event/APICallEvent.java | 21 + .../lowcoder/infra/event/AbstractEvent.java | 48 +- .../java/org/lowcoder/infra/event/Event.java | 6 - .../org/lowcoder/infra/event/EventType.java | 63 -- .../infra/event/SystemCommonEvent.java | 18 + .../event/datasource/DatasourceEvent.java | 1 - .../datasource/DatasourcePermissionEvent.java | 1 - .../infra/event/group/GroupCreateEvent.java | 2 - .../infra/event/group/GroupDeleteEvent.java | 2 - .../infra/event/group/GroupUpdateEvent.java | 2 - .../groupmember/GroupMemberAddEvent.java | 2 - .../groupmember/GroupMemberLeaveEvent.java | 2 - .../groupmember/GroupMemberRemoveEvent.java | 2 - .../GroupMemberRoleUpdateEvent.java | 2 - .../infra/event/user/UserLoginEvent.java | 1 - .../infra/event/user/UserLogoutEvent.java | 1 - .../infra/localcache/ReloadableCache.java | 2 +- .../infra/serverlog/ServerLogService.java | 11 + .../clickHousePlugin/plugin.properties | 5 - .../elasticSearchPlugin/plugin.properties | 5 - .../googleSheetsPlugin/plugin.properties | 5 - .../graphqlPlugin/plugin.properties | 5 - .../lowcoderApiPlugin/plugin.properties | 5 - .../mongoPlugin/plugin.properties | 5 - .../mssqlPlugin/plugin.properties | 5 - .../mysqlPlugin/plugin.properties | 5 - .../oraclePlugin/plugin.properties | 5 - .../lowcoder-plugins/oraclePlugin/pom.xml | 3 + server/api-service/lowcoder-plugins/pom.xml | 8 + .../postgresPlugin/plugin.properties | 5 - .../redisPlugin/plugin.properties | 5 - .../restApiPlugin/plugin.properties | 5 - .../smtpPlugin/plugin.properties | 5 - .../snowflakePlugin/plugin.properties | 5 - server/api-service/lowcoder-sdk/pom.xml | 17 +- .../org/lowcoder/sdk/config/CommonConfig.java | 8 + .../api-service/lowcoder-server/cert/README | 33 + .../lowcoder-server/cert/signing.p12 | Bin 0 -> 4434 bytes server/api-service/lowcoder-server/pom.xml | 623 ++++++++++-------- .../src/main/assembly/assembly.xml | 58 ++ .../org/lowcoder/api/ServerApplication.java | 3 + .../application/ApplicationController.java | 14 +- .../service/AuthenticationApiServiceImpl.java | 10 +- .../api/datasource/DatasourceController.java | 12 +- .../ApplicationConfiguration.java | 15 + .../CustomWebFluxConfigurationSupport.java | 16 + .../configuration/PluginConfiguration.java | 58 ++ .../api/framework/filter/APIDelayFilter.java | 38 ++ .../api/framework/filter/FilterOrder.java | 2 + .../filter/ReactiveRequestContextFilter.java | 18 + .../filter/ReactiveRequestContextHolder.java | 13 + .../framework/filter/ThrottlingFilter.java | 2 +- .../plugin/LowcoderPluginManager.java | 130 ++++ .../plugin/PathBasedPluginLoader.java | 140 ++++ .../framework/plugin/PluginClassLoader.java | 104 +++ .../api/framework/plugin/PluginExecutor.java | 36 + .../api/framework/plugin/PluginLoader.java | 11 + .../plugin/SharedPluginServices.java | 59 ++ .../plugin/data/PluginServerRequest.java | 198 ++++++ .../endpoint/PluginEndpointHandler.java | 15 + .../endpoint/PluginEndpointHandlerImpl.java | 195 ++++++ .../ReloadableRouterFunctionMapping.java | 20 + .../security/PluginAuthorizationManager.java | 94 +++ .../framework/security/SecurityConfig.java | 78 ++- .../lowcoder/api/home/FolderApiService.java | 4 +- .../lowcoder/api/home/FolderController.java | 8 +- .../lowcoder/api/home/SessionUserService.java | 4 + .../api/home/SessionUserServiceImpl.java | 12 + .../api/query/LibraryQueryController.java | 2 +- .../api/usermanagement/GroupApiService.java | 17 +- .../api/usermanagement/OrgApiServiceImpl.java | 2 +- .../api/usermanagement/OrgDevChecker.java | 2 +- .../api/usermanagement/UserApiService.java | 2 +- .../api/util/ApiCallEventPublisher.java | 90 +++ .../api/util/BusinessEventPublisher.java | 171 +++-- .../util/RandomPasswordGeneratorConfig.java | 28 + .../runner/migrations/DatabaseChangelog.java | 8 +- .../migrations/job/AddSuperAdminUser.java | 6 + .../migrations/job/AddSuperAdminUserImpl.java | 67 ++ .../main/resources/application-lowcoder.yml | 12 + .../resources/selfhost/ce/application.yml | 4 +- server/api-service/pom.xml | 441 ++++--------- 101 files changed, 2919 insertions(+), 905 deletions(-) create mode 100644 server/api-service/PLUGIN.md create mode 100644 server/api-service/distribution/pom.xml create mode 100644 server/api-service/distribution/src/assembly/bin.xml create mode 100755 server/api-service/distribution/src/assembly/set-classpath.sh create mode 100644 server/api-service/lowcoder-dependencies/pom.xml delete mode 100644 server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java create mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java delete mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java delete mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java create mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java delete mode 100644 server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/redisPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties create mode 100644 server/api-service/lowcoder-server/cert/README create mode 100644 server/api-service/lowcoder-server/cert/signing.p12 create mode 100644 server/api-service/lowcoder-server/src/main/assembly/assembly.xml create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java diff --git a/.gitignore b/.gitignore index 2e1b56cd5..81c0e6c82 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ client/node_modules/ client/packages/lowcoder-plugin-demo/.yarn/install-state.gz client/packages/lowcoder-plugin-demo/yarn.lock client/packages/lowcoder-plugin-demo/.yarn/cache/@types-node-npm-16.18.68-56f72825c0-094ae9ed80.zip +application-dev.yml diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 6f55ed0fc..c536b8a24 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -2,20 +2,14 @@ ## Build Lowcoder api-service application ## FROM maven:3.9-eclipse-temurin-17 AS build-api-service + +# Build lowcoder-api COPY ./server/api-service /lowcoder-server WORKDIR /lowcoder-server RUN --mount=type=cache,target=/root/.m2 mvn -f pom.xml clean package -DskipTests # Create required folder structure -RUN mkdir -p /lowcoder/api-service/plugins /lowcoder/api-service/config /lowcoder/api-service/logs - -# Define lowcoder main jar and plugin jars -ARG JAR_FILE=/lowcoder-server/lowcoder-server/target/lowcoder-server-*.jar -ARG PLUGIN_JARS=/lowcoder-server/lowcoder-plugins/*/target/*.jar - -# Copy lowcoder server application and plugins -RUN cp ${JAR_FILE} /lowcoder/api-service/server.jar \ - && cp ${PLUGIN_JARS} /lowcoder/api-service/plugins/ +RUN mkdir -p /lowcoder/api-service/config /lowcoder/api-service/logs /lowcoder/plugins # Copy lowcoder server configuration COPY server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml /lowcoder/api-service/config/ @@ -43,6 +37,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends gosu \ # Copy lowcoder server configuration COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder/api-service /lowcoder/api-service +# Copy lowcoder api service app, dependencies and libs +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/app /lowcoder/api-service/app +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/dependencies /lowcoder/api-service/dependencies +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/libs /lowcoder/api-service/libs +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/plugins /lowcoder/api-service/plugins +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/set-classpath.sh /lowcoder/api-service/set-classpath.sh + EXPOSE 8080 CMD [ "sh" , "/lowcoder/api-service/entrypoint.sh" ] @@ -202,6 +203,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-instal # Add lowcoder api-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-api-service /lowcoder/api-service /lowcoder/api-service +RUN mkdir -p /lowcoder/plugins/ && chown lowcoder:lowcoder /lowcoder/plugins/ # Add lowcoder node-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-node-service /lowcoder/node-service /lowcoder/node-service diff --git a/deploy/docker/api-service/entrypoint.sh b/deploy/docker/api-service/entrypoint.sh index 5f2e3ad2e..0f43580fe 100644 --- a/deploy/docker/api-service/entrypoint.sh +++ b/deploy/docker/api-service/entrypoint.sh @@ -27,12 +27,16 @@ ${JAVA_HOME}/bin/java -version echo cd /lowcoder/api-service +source set-classpath.sh + exec gosu ${USER_ID}:${GROUP_ID} ${JAVA_HOME}/bin/java \ + -Djava.util.prefs.userRoot=/tmp \ -Djava.security.egd=file:/dev/./urandom \ -Dhttps.protocols=TLSv1.1,TLSv1.2 \ -Dlog4j2.formatMsgNoLookups=true \ -Dspring.config.location="file:///lowcoder/api-service/config/application.yml,file:///lowcoder/api-service/config/application-selfhost.yml" \ --add-opens java.base/java.nio=ALL-UNNAMED \ + -cp "${LOWCODER_CLASSPATH:=.}" \ ${JAVA_OPTS} \ - -jar "${APP_JAR}" --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES} + org.lowcoder.api.ServerApplication --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES} diff --git a/server/api-service/.gitignore b/server/api-service/.gitignore index 044c6298e..a9fc541a9 100644 --- a/server/api-service/.gitignore +++ b/server/api-service/.gitignore @@ -23,8 +23,9 @@ dependency-reduced-pom.xml .run/** logs/** tmp/** -/openblocks-server/logs/ +# Ignore plugin.properties which are generated dynamically +**/plugin.properties # to ignore the node_modeules folder node_modules @@ -34,5 +35,4 @@ package-lock.json # test coverage coverage-summary.json app/client/cypress/locators/Widgets.json -/openblocks-domain/logs/ -application-lowcoder.yml \ No newline at end of file +application-lowcoder.yml diff --git a/server/api-service/PLUGIN.md b/server/api-service/PLUGIN.md new file mode 100644 index 000000000..92fb50ad9 --- /dev/null +++ b/server/api-service/PLUGIN.md @@ -0,0 +1,122 @@ +# Lowcoder plugin system (WIP) + +This is an ongoing effort to refactor current plugin system based on pf4j library. + +## Reasoning + +1. create a cleaner and simpler plugin system with clearly defined purpose(s) (new endpoints, new datasource types, etc..) +2. lowcoder does not need live plugin loading/reloading/unloading/updates, therefore the main feature of pf4j is rendered useless, in fact it adds a lot of complexity due to classloaders used for managing plugins (especially in spring/boot applications) +3. simpler and easier plugin detection - just a jar with a class implementing a common interface (be it a simple pojo project or a complex spring/boot implementation) + +## How it works + +The main entrypoint for plugin system is in **lowcoder-server** module with class **org.lowcoder.api.framework.configuration.PluginConfiguration** +It creates: +- LowcoderPluginManager bean which is responsible for plugin lifecycle management +- Adds plugin defined endpoints to lowcoder by creating **pluginEndpoints** bean +- TODO: Adds plugin defined datasources to lowcoder by creating **pluginDatasources** bean + +### lowcoder-plugin-api library + +This library contains APIs for plugin implementations. +It is used by both, lowcoder API server as well as all plugins. + +### PluginLoader + +The sole purpose of a PluginLoader is to find plugin candidates and load them into VM. +There is currently one implementation that based on paths - **PathBasedPluginLoader**, it: +- looks in folders and subfolders defined in **application.yaml** - entries can point to a folder or specific jar file. If a relative path is supplied, the location of lowcoder API server application jar is used as parent folder (when run in non-packaged state, eg. in IDE, it uses the folder where ServerApplication.class is generated) + +```yaml +common: + plugin-dirs: + - plugins + - /some/custom/path/myGreatPlugin.jar +``` +- finds all **jar**(s) and inspects them for classes implementing **LowcoderPlugin** interface +- instantiates all LowcoderPlugin implementations + +### LowcoderPluginManager + +The main job of plugin manager is to: +- register plugins found and instantiated by **PluginLoader** +- start registered plugins by calling **LowcoderPlugin.load()** method +- create and register **RouterFunction**(s) for all loaded plugin endpoints +- TODO: create and register datasources for all loaded plugin datasources + +## Plugin project structure + +Plugin jar can be structured in any way you like. It can be a plain java project, but also a spring/boot based project or based on any other framework. + +It is composed from several parts: +- class(es) implementing **LowcoderPlugin** interface +- class(es) implementing **LowcoderEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: + +```java + @EndpointExtension(uri = , method = ) + public Mono (ServerRequest request) + { + ... your endpoint logic implementation + } + + for example: + + @EndpointExtension(uri = "/hello-world", method = Method.GET) + public Mono helloWorld(ServerRequest request) + { + return ServerResponse.ok().body(Mono.just(Hello.builder().message("Hello world!").build()), Hello.class); + } +``` +- TODO: class(es) impelemting **LowcoderDatasource** interface + +### LowcoderPlugin implementations + +Methods of interest: +- **pluginId()** - unique plugin ID - if a plugin with such ID is already loaded, subsequent plugins whith this ID will be ignored +- **description()** - short plugin description +- **load(ApplicationContext parentContext)** - is called during plugin startup - this is the place where you should completely initialize your plugin. If initialization fails, return false +- **unload()** - is called during lowcoder API server shutdown - this is the place where you should release all resources +- **endpoints()** - needs to contain all initialized **PluginEndpoints** you want to expose, for example: + +```java + @Override + public List endpoints() + { + List endpoints = new ArrayList<>(); + + endpoints.add(new HelloWorldEndpoint()); + + return endpoints; + } +``` +- **pluginInfo()** - should return a record object with additional information about your plugin. It is serialized to JSON as part of the **/plugins** listing (see **"info"** object in this example): + +```json +[ + { + "id": "example-plugin", + "description": "Example plugin for lowcoder platform", + "info": {} + }, + { + "id": "enterprise", + "description": "Lowcoder enterprise plugin", + "info": { + "enabledFeatures": [ + "endpointApiUsage" + ] + } + } +] +``` + +## TODOs + +1. Implement endpoint security - currently all plugin endpoints are public (probably by adding **security** attribute to **@EndpointExtension** and enforcing it) + + +## QUESTIONS / CONSIDERATIONS + +1. currently the plugin endpoints are prefixed with **/plugin/{pluginId}/** - this is hardcoded, do we want to make it configurable? + + diff --git a/server/api-service/distribution/pom.xml b/server/api-service/distribution/pom.xml new file mode 100644 index 000000000..d68b3fab4 --- /dev/null +++ b/server/api-service/distribution/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + + org.lowcoder + lowcoder-root + ${revision} + + + distribution + pom + + + ${project.build.directory}/dependencies + + + + + + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder + lowcoder-server + + + + + lowcoder-api-service + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${assembly.lib.directory} + false + false + true + true + + + + + + maven-assembly-plugin + + + distro-assembly + package + + single + + + false + + src/assembly/bin.xml + + + + + + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/bin.xml b/server/api-service/distribution/src/assembly/bin.xml new file mode 100644 index 000000000..b6422619e --- /dev/null +++ b/server/api-service/distribution/src/assembly/bin.xml @@ -0,0 +1,72 @@ + + bin + + dir + + false + + + + src/assembly/set-classpath.sh + + + + + + ${assembly.lib.directory} + dependencies + + ${project.groupId}:* + + + + + + + + true + + org.lowcoder:lowcoder-server + + + app + false + false + + + + + + true + + org.lowcoder:lowcoder-domain + org.lowcoder:lowcoder-infra + org.lowcoder:lowcoder-sdk + + + libs + false + false + + + + + + true + true + + org.lowcoder:*Plugin + + + org.lowcoder:sqlBasedPlugin + + + plugins + false + false + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/set-classpath.sh b/server/api-service/distribution/src/assembly/set-classpath.sh new file mode 100755 index 000000000..de82ddf7f --- /dev/null +++ b/server/api-service/distribution/src/assembly/set-classpath.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# +# Set lowcoder api service classpath for use in startup script +# +export LOWCODER_CLASSPATH="`find libs/ dependencies/ app/ -type f -name "*.jar" | tr '\n' ':' | sed -e 's/:$//'`" + +# +# Example usage: +# +# java -cp "${LOWCODER_CLASSPATH}" org.lowcoder.api.ServerApplication diff --git a/server/api-service/lowcoder-dependencies/pom.xml b/server/api-service/lowcoder-dependencies/pom.xml new file mode 100644 index 000000000..53ffadf95 --- /dev/null +++ b/server/api-service/lowcoder-dependencies/pom.xml @@ -0,0 +1,224 @@ + + + + + lowcoder-root + org.lowcoder + ${revision} + + + 4.0.0 + lowcoder-dependencies + pom + + + + + org.springframework.boot + spring-boot-dependencies + 3.1.2 + pom + import + + + + org.lowcoder.plugin + lowcoder-plugin-api + 2.3.0 + + + + org.pf4j + pf4j + 3.5.0 + + + + org.json + json + 20230227 + + + + org.projectlombok + lombok + 1.18.26 + + + + org.apache.commons + commons-text + 1.10.0 + + + commons-io + commons-io + 2.13.0 + + + org.glassfish + javax.el + 3.0.0 + + + javax.el + javax.el-api + 3.0.0 + + + + org.eclipse.jgit + org.eclipse.jgit + 6.5.0.202303070854-r + + + + org.apache.commons + commons-collections4 + 4.4 + + + com.google.guava + guava + 30.0-jre + + + + tv.twelvetone.rjson + rjson + 1.3.1-SNAPSHOT + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + 1.6.21 + + + + com.jayway.jsonpath + json-path + 2.7.0 + + + com.github.ben-manes.caffeine + caffeine + 3.0.5 + + + es.moki.ratelimitj + ratelimitj-core + 0.7.0 + + + com.github.spullara.mustache.java + compiler + 0.9.6 + + + + es.moki.ratelimitj + ratelimitj-redis + 0.7.0 + + + + io.projectreactor + reactor-core + 3.4.29 + + + + org.pf4j + pf4j-spring + 0.8.0 + + + + com.querydsl + querydsl-apt + 5.0.0 + + + + io.sentry + sentry-spring-boot-starter + 3.1.2 + + + + org.jgrapht + jgrapht-core + 1.5.0 + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + javax.activation + activation + 1.1.1 + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.3 + + + + com.github.cloudyrock.mongock + mongock-bom + 4.3.8 + pom + import + + + + io.projectreactor.tools + blockhound + 1.0.6.RELEASE + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + + + + io.projectreactor + reactor-test + 3.3.5.RELEASE + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + 4.7.0 + + + org.mockito + mockito-inline + 5.2.0 + test + + + javax.validation + validation-api + 2.0.1.Final + + + + + + + diff --git a/server/api-service/lowcoder-domain/pom.xml b/server/api-service/lowcoder-domain/pom.xml index 2150c484a..d7b96e027 100644 --- a/server/api-service/lowcoder-domain/pom.xml +++ b/server/api-service/lowcoder-domain/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -186,6 +186,12 @@ es.moki.ratelimitj ratelimitj-redis + + + io.lettuce + lettuce-core + + @@ -242,6 +248,18 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + com.mysema.maven apt-maven-plugin @@ -268,9 +286,21 @@ UTF-8 + UTF-8 + 17 - 17 - 17 + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java deleted file mode 100644 index 18d73fdf5..000000000 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.lowcoder.domain.configurations; - -import org.pf4j.spring.SpringPluginManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class Pf4jConfiguration { - - @Bean - public SpringPluginManager pluginManager() { - return new SpringPluginManager(); - } - -} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java index 2a754bb6d..634a8cdb1 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java @@ -36,6 +36,11 @@ public boolean isAdmin() { return role == MemberRole.ADMIN; } + public boolean isSuperAdmin() { + return role == MemberRole.SUPER_ADMIN; + } + + @JsonIgnore public boolean isInvalid() { return this == NOT_EXIST || StringUtils.isBlank(groupId); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java index 5aefdbae6..7e7a9daf0 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java @@ -7,7 +7,8 @@ public enum MemberRole { MEMBER("member"), - ADMIN("admin"); + ADMIN("admin"), + SUPER_ADMIN("super_admin"); private static final Map VALUE_MAP; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java index 66e83f49e..5e990485a 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java @@ -52,6 +52,10 @@ public MemberRole getRole() { return role; } + public boolean isSuperAdmin() { + return role == MemberRole.SUPER_ADMIN; + } + public boolean isAdmin() { return role == MemberRole.ADMIN; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java index 4dc918374..5a4d82ec6 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java @@ -17,9 +17,9 @@ public interface OrganizationService { @PossibleEmptyMono Mono getOrganizationInEnterpriseMode(); - Mono create(Organization organization, String creatorUserId); + Mono create(Organization organization, String creatorUserId, boolean isSuperAdmin); - Mono createDefault(User user); + Mono createDefault(User user, boolean isSuperAdmin); Mono getById(String id); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 48e4bc6de..9b9da9549 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -86,7 +86,7 @@ public OrganizationServiceImpl(ConfigCenter configCenter) { } @Override - public Mono createDefault(User user) { + public Mono createDefault(User user, boolean isSuperAdmin) { return Mono.deferContextual(contextView -> { Locale locale = getLocale(contextView); String userOrgSuffix = getMessage(locale, "USER_ORG_SUFFIX"); @@ -96,7 +96,7 @@ public Mono createDefault(User user) { organization.setIsAutoGeneratedOrganization(true); // saas mode if (commonConfig.getWorkspace().getMode() == WorkspaceMode.SAAS) { - return create(organization, user.getId()); + return create(organization, user.getId(), isSuperAdmin); } // enterprise mode return joinOrganizationInEnterpriseMode(user.getId()) @@ -107,7 +107,7 @@ public Mono createDefault(User user) { OrganizationDomain organizationDomain = new OrganizationDomain(); organizationDomain.setConfigs(List.of(DEFAULT_AUTH_CONFIG)); organization.setOrganizationDomain(organizationDomain); - return create(organization, user.getId()); + return create(organization, user.getId(), isSuperAdmin); }); }); } @@ -145,7 +145,7 @@ private Mono getByEnterpriseOrgId() { } @Override - public Mono create(Organization organization, String creatorId) { + public Mono create(Organization organization, String creatorId, boolean isSuperAdmin) { return Mono.defer(() -> { if (organization == null || StringUtils.isNotBlank(organization.getId())) { @@ -155,19 +155,19 @@ public Mono create(Organization organization, String creatorId) { return Mono.just(organization); }) .flatMap(repository::save) - .flatMap(newOrg -> onOrgCreated(creatorId, newOrg)) + .flatMap(newOrg -> onOrgCreated(creatorId, newOrg, isSuperAdmin)) .log(); } - private Mono onOrgCreated(String userId, Organization newOrg) { + private Mono onOrgCreated(String userId, Organization newOrg, boolean isSuperAdmin) { return groupService.createAllUserGroup(newOrg.getId()) .then(groupService.createDevGroup(newOrg.getId())) - .then(setOrgAdmin(userId, newOrg)) + .then(setOrgAdmin(userId, newOrg, isSuperAdmin)) .thenReturn(newOrg); } - private Mono setOrgAdmin(String userId, Organization newOrg) { - return orgMemberService.addMember(newOrg.getId(), userId, MemberRole.ADMIN); + private Mono setOrgAdmin(String userId, Organization newOrg, boolean isSuperAdmin) { + return orgMemberService.addMember(newOrg.getId(), userId, isSuperAdmin ? MemberRole.SUPER_ADMIN : MemberRole.ADMIN); } @Override diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java index 8b0587480..3841a42b9 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java @@ -66,7 +66,7 @@ public Mono>> getAllMatchingPermissions(Str return getOrgId(resourceIds.iterator().next()) .flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId)) .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(buildAdminPermissions(resourceType, resourceIds, userId)); } return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, resourceIds, resourceAction); @@ -112,7 +112,7 @@ public Mono checkUserPermissionStatusOnResource( Mono orgUserPermissionMono = getOrgId(resourceId) .flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId)) .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(UserPermissionOnResourceStatus.success(buildAdminPermission(resourceType, resourceId, userId))); } return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, Collections.singleton(resourceId), resourceAction) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index e7526be8d..16ac4f8e2 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -335,7 +335,7 @@ protected Mono>> buildUserDetailGroups(String userId, O Locale locale) { String orgId = orgMember.getOrgId(); Flux groups; - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { groups = groupService.getByOrgId(orgId).sort(); } else { if (withoutDynamicGroups) { diff --git a/server/api-service/lowcoder-infra/pom.xml b/server/api-service/lowcoder-infra/pom.xml index 5c34fde9c..39a8a8640 100644 --- a/server/api-service/lowcoder-infra/pom.xml +++ b/server/api-service/lowcoder-infra/pom.xml @@ -127,14 +127,33 @@ org.springframework.boot spring-boot-starter-webflux + + org.lowcoder.plugin + lowcoder-plugin-api + UTF-8 + UTF-8 + 17 + 17 17 + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java new file mode 100644 index 000000000..f000e640f --- /dev/null +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java @@ -0,0 +1,21 @@ +package org.lowcoder.infra.event; + +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.springframework.util.MultiValueMap; + +@Getter +@SuperBuilder +public class APICallEvent extends AbstractEvent { + + private final EventType type; + private final String httpMethod; + private final String requestUri; + private final MultiValueMap headers; + private final MultiValueMap queryParams; + + @Override + public EventType getEventType() { + return EventType.API_CALL_EVENT; + } +} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java index 018ec9894..c11381cd2 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java @@ -1,12 +1,56 @@ package org.lowcoder.infra.event; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.lowcoder.plugin.api.event.LowcoderEvent; + import lombok.Getter; import lombok.experimental.SuperBuilder; @Getter @SuperBuilder -public abstract class AbstractEvent implements Event { - +public abstract class AbstractEvent implements LowcoderEvent +{ protected final String orgId; protected final String userId; + protected final String sessionHash; + protected final Boolean isAnonymous; + private final String ipAddress; + protected Map details; + + public Map details() + { + return this.details; + } + + public static abstract class AbstractEventBuilder> + { + public B detail(String name, String value) + { + if (details == null) + { + details = new HashMap<>(); + } + this.details.put(name, value); + return self(); + } + } + + public void populateDetails() { + if (details == null) { + details = new HashMap<>(); + } + for(Field f : getClass().getDeclaredFields()){ + Object value = null; + try { + f.setAccessible(Boolean.TRUE); + value = f.get(this); + details.put(f.getName(), value); + } catch (Exception e) { + } + + } + } } diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java deleted file mode 100644 index 29dd3a36c..000000000 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.lowcoder.infra.event; - -public interface Event { - - EventType getEventType(); -} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java deleted file mode 100644 index 52260736f..000000000 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.lowcoder.infra.event; - -import java.util.Locale; - -import org.lowcoder.sdk.util.LocaleUtils; - -public enum EventType { - - USER_LOGIN("EVENT_TYPE_USER_LOGIN"), - USER_LOGOUT("EVENT_TYPE_USER_LOGOUT"), - - // application - VIEW("EVENT_TYPE_VIEW"), - APPLICATION_CREATE("EVENT_TYPE_APPLICATION_CREATE"), - APPLICATION_DELETE("EVENT_TYPE_APPLICATION_DELETE"), - APPLICATION_UPDATE("EVENT_TYPE_APPLICATION_UPDATE"), - APPLICATION_MOVE("EVENT_TYPE_APPLICATION_MOVE"), - APPLICATION_RECYCLED("EVENT_TYPE_APPLICATION_RECYCLED"), - APPLICATION_RESTORE("EVENT_TYPE_APPLICATION_RESTORE"), - - // folder - FOLDER_CREATE("EVENT_TYPE_FOLDER_CREATE"), - FOLDER_DELETE("EVENT_TYPE_FOLDER_DELETE"), - FOLDER_UPDATE("EVENT_TYPE_FOLDER_UPDATE"), - - // query - QUERY_EXECUTION("EVENT_TYPE_QUERY_EXECUTION"), - // group - GROUP_CREATE("EVENT_TYPE_GROUP_CREATE"), - GROUP_UPDATE("EVENT_TYPE_GROUP_UPDATE"), - GROUP_DELETE("EVENT_TYPE_GROUP_DELETE"), - GROUP_MEMBER_ADD("EVENT_TYPE_GROUP_MEMBER_ADD"), - GROUP_MEMBER_ROLE_UPDATE("EVENT_TYPE_GROUP_MEMBER_ROLE_UPDATE"), - GROUP_MEMBER_LEAVE("EVENT_TYPE_GROUP_MEMBER_LEAVE"), - GROUP_MEMBER_REMOVE("EVENT_TYPE_GROUP_MEMBER_REMOVE"), - //system - SERVER_START_UP("EVENT_TYPE_SERVER_START_UP"), - - // data source - DATA_SOURCE_CREATE("DATA_SOURCE_CREATE"), - DATA_SOURCE_UPDATE("DATA_SOURCE_UPDATE"), - DATA_SOURCE_DELETE("DATA_SOURCE_DELETE"), - DATA_SOURCE_PERMISSION_GRANT("DATA_SOURCE_PERMISSION_GRANT"), - DATA_SOURCE_PERMISSION_UPDATE("DATA_SOURCE_PERMISSION_UPDATE"), - DATA_SOURCE_PERMISSION_DELETE("DATA_SOURCE_PERMISSION_DELETE"), - - // library query - LIBRARY_QUERY_CREATE("LIBRARY_QUERY_CREATE"), - LIBRARY_QUERY_UPDATE("LIBRARY_QUERY_UPDATE"), - LIBRARY_QUERY_DELETE("LIBRARY_QUERY_DELETE"), - LIBRARY_QUERY_PUBLISH("LIBRARY_QUERY_PUBLISH"), - ; - - private final String desc; - - EventType(String desc) { - this.desc = desc; - } - - public String getDesc(Locale locale) { - return LocaleUtils.getMessage(locale, this.desc); - } -} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java new file mode 100644 index 000000000..5ddacf5c1 --- /dev/null +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java @@ -0,0 +1,18 @@ +package org.lowcoder.infra.event; + +import org.checkerframework.checker.units.qual.C; + +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class SystemCommonEvent extends AbstractEvent +{ + private final long apiCalls; + + @Override + public EventType getEventType() { + return EventType.SERVER_INFO; + } +} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java index 7c724b68d..4c5471d68 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java @@ -1,7 +1,6 @@ package org.lowcoder.infra.event.datasource; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java index 9e967e248..99d2703cb 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java @@ -3,7 +3,6 @@ import java.util.Collection; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java index d2983a29c..ab80e0cc0 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java index 4da2b51e3..2d7caa495 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java index ac6ef697d..9d06c459a 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java index bf5bcd89f..52c17df48 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java index bd43fa482..d35db5198 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java index 888da0aff..6b4fef1d2 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java index 62ea39478..785a28fc5 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java index c0e7fafd2..aa840de74 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java @@ -1,7 +1,6 @@ package org.lowcoder.infra.event.user; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java index 8e0a8b073..cf2fdd714 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java @@ -1,7 +1,6 @@ package org.lowcoder.infra.event.user; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java index 0eb36e585..f50939f94 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java @@ -83,7 +83,7 @@ public ReloadableCache build() { private void startScheduledReloadTask(ReloadableCache cache) { ScheduledExecutorService scheduledExecutor = newSingleThreadScheduledExecutor(); scheduledExecutor.scheduleAtFixedRate(() -> { - log.debug("{} scheduled reload...", cacheName); + log.trace("{} scheduled reload...", cacheName); try { cache.cachedValue = factory.getValue().block(); } catch (Exception e) { diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java index b45708a20..6faf54dc7 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java @@ -10,8 +10,10 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.collections4.CollectionUtils; +import org.lowcoder.infra.event.SystemCommonEvent; import org.lowcoder.infra.perf.PerfHelper; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -27,6 +29,9 @@ public class ServerLogService { @Autowired private PerfHelper perfHelper; + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + private volatile Queue serverLogs = new ConcurrentLinkedQueue<>(); public void record(ServerLog serverLog) { @@ -43,7 +48,13 @@ private void scheduledInsert() { serverLogRepository.saveAll(tmp) .collectList() .subscribe(result -> { + int count = result.size(); perfHelper.count(SERVER_LOG_BATCH_INSERT, Tags.of("size", String.valueOf(result.size()))); + applicationEventPublisher.publishEvent(SystemCommonEvent.builder() + .apiCalls(count) + .detail("apiCalls", Integer.toString(count)) + .build() + ); }); } diff --git a/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties b/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties deleted file mode 100644 index 822e4fa85..000000000 --- a/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=clickHouse-plugin -plugin.class=org.lowcoder.plugin.clickhouse.ClickHousePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties b/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties deleted file mode 100644 index 87717ad57..000000000 --- a/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=es-plugin -plugin.class=org.lowcoder.plugin.es.EsPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties b/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties deleted file mode 100644 index 7c9cd8c66..000000000 --- a/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=googleSheets-plugin -plugin.class=org.lowcoder.plugin.googlesheets.GoogleSheetsPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties deleted file mode 100644 index 5d4dd5bba..000000000 --- a/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=graphql-plugin -plugin.class=org.lowcoder.plugin.graphql.GraphQLPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties b/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties deleted file mode 100644 index 545de1ba2..000000000 --- a/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=lowcoder-api-plugin -plugin.class=org.lowcoder.plugin.LowcoderApiPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties deleted file mode 100644 index a18bf7f80..000000000 --- a/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mongo-plugin -plugin.class=org.lowcoder.plugin.mongo.MongoPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties deleted file mode 100644 index 002e43851..000000000 --- a/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mssql-plugin -plugin.class=org.lowcoder.plugin.mssql.MssqlPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties deleted file mode 100644 index 2e2c88008..000000000 --- a/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mysql-plugin -plugin.class=org.lowcoder.plugin.mysql.MysqlPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties b/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties deleted file mode 100644 index 516f2de00..000000000 --- a/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=oracle-plugin -plugin.class=org.lowcoder.plugin.oracle.OraclePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml b/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml index fcd91b289..67eb51702 100644 --- a/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml +++ b/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml @@ -13,6 +13,9 @@ + UTF-8 + UTF-8 + 17 17 diff --git a/server/api-service/lowcoder-plugins/pom.xml b/server/api-service/lowcoder-plugins/pom.xml index 11807e458..90512a3f5 100644 --- a/server/api-service/lowcoder-plugins/pom.xml +++ b/server/api-service/lowcoder-plugins/pom.xml @@ -79,6 +79,14 @@ + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + org.lowcoder sqlBasedPlugin diff --git a/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties b/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties deleted file mode 100644 index bbd887fb0..000000000 --- a/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=postgres-plugin -plugin.class=org.lowcoder.plugin.postgres.PostgresPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties b/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties deleted file mode 100644 index ded41c272..000000000 --- a/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=redis-plugin -plugin.class=org.lowcoder.plugin.redis.RedisPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties b/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties deleted file mode 100644 index 0ed0b7d87..000000000 --- a/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=restapi-plugin -plugin.class=org.lowcoder.plugin.restapi.RestApiPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties b/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties deleted file mode 100644 index 70d475de9..000000000 --- a/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=smtp-plugin -plugin.class=org.lowcoder.plugins.SmtpPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties b/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties deleted file mode 100644 index 5f7dbca58..000000000 --- a/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=snowflake-plugin -plugin.class=org.lowcoder.plugin.snowflake.SnowflakePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-sdk/pom.xml b/server/api-service/lowcoder-sdk/pom.xml index cbd69d47c..9918359bc 100644 --- a/server/api-service/lowcoder-sdk/pom.xml +++ b/server/api-service/lowcoder-sdk/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -15,6 +15,8 @@ UTF-8 + UTF-8 + 17 @@ -171,4 +173,17 @@ validation-api + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index d1fcf3ea8..8334e5562 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -44,6 +44,8 @@ public class CommonConfig { private Cookie cookie = new Cookie(); private JsExecutor jsExecutor = new JsExecutor(); private Set disallowedHosts = new HashSet<>(); + private List pluginDirs = new ArrayList<>(); + private SuperAdmin superAdmin = new SuperAdmin(); private Marketplace marketplace = new Marketplace(); public boolean isSelfHost() { @@ -158,4 +160,10 @@ public static class Marketplace { public static class Query { private long readStructureTimeout = 15000; } + + @Data + public static class SuperAdmin { + private String userName; + private String password; + } } diff --git a/server/api-service/lowcoder-server/cert/README b/server/api-service/lowcoder-server/cert/README new file mode 100644 index 000000000..0589816e8 --- /dev/null +++ b/server/api-service/lowcoder-server/cert/README @@ -0,0 +1,33 @@ +To generate the signing keys in PKCS#12 format: + +$ keytool -genkey -alias dev -keyalg RSA -keysize 4096 -validity 36500 -keystore signing.p12 -storetype pkcs12 + +Enter keystore password: +Re-enter new password: +What is your first and last name? + [Unknown]: dev.lowcoder.org +What is the name of your organizational unit? + [Unknown]: dev +What is the name of your organization? + [Unknown]: Lowcoder Software LTD +What is the name of your City or Locality? + [Unknown]: London +What is the name of your State or Province? + [Unknown]: United Kingdom +What is the two-letter country code for this unit? + [Unknown]: UK +Is CN=dev.lowcoder.org, OU=dev, O=Lowcoder Software LTD, L=London, ST=United Kingdom, C=UK correct? + [no]: yes + +Generating 4,096 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 36,500 days + for: CN=dev.lowcoder.org, OU=dev, O=Lowcoder Software LTD, L=London, ST=United Kingdom, C=UK + + + +To export the public key from generated key pair: + +$ openssl rsa -in signing.p12 -pubout -out lowcoder.pub + +Enter pass phrase for PKCS12 import pass phrase: +writing RSA key + diff --git a/server/api-service/lowcoder-server/cert/signing.p12 b/server/api-service/lowcoder-server/cert/signing.p12 new file mode 100644 index 0000000000000000000000000000000000000000..2f336a1f6d694ca3ca35e2449cf903933bfd36fd GIT binary patch literal 4434 zcma)=XEYp)v&QXWSfTSRLBY;o}bym;h| z5^o=tF=15?j3f3oIJ9!{5a6dqZI$1=v}%W*pS0Fzd|e&CF%j@f<2ig4{1dNPhH5jP zTPBI_Oof?Y`uinLC;_hRP0`vTizS(%Lkplz_(zPyCR?}F#HLh^MR%HIBEb6M@ zeL=|QrKgX&>yx=i0Bo*4AzfN{6cp6rQiak(%cP`*MXytWQ!f1hU&Jm#K=8<8n#ylH}hx z+O3`0rMxINsd!SU^yD=+QLXN5#?pMoQ%-LVymO&_AI>>MSz)3w4KyiYqNVs1wK20t zfAM}F6FaTki9lVA$j%oX@hRa@QYt`O%moy+mkAJzzE2wvHZ0U-)Glg7PPtv*`$R6d zBy~jO9~w>_k-3hdEu$tnTZtK6lIgYn(sWu+Z^zl!pSk-XU2E#v@vyCWMi!0cfV+Lk z{iHARRG)?C@Fl#w|EiA^`FK9|Buuw6yl+jDIH5K|eST7kc~sHk79{9k0~dPL3Dy@( zT29kJscNVBhSg|4T88cJpg@3%LX=(TW0U-^o1X{BCVhybWyN6SiEy01aLaA*eqvj; z1cH_`7b_XAtp)*E(>-Zc@iD2DSd78-+J05LY~Or&z{PBK0Qh%T60W~cO>#)K%MP9id2(d3sp-H6Fk5@@Zh%z;Ko{j^Ca$1ES1QR?IM&Jl9p9(}2z z)*u-(@3{zEvkoEL%Se%^0R+kI*}#DG}>C;x2z-9f2hAGjMq(UQv@E!p}Vt5YpL` zPiwlf{fD~93zgb-BsThp`L=`)sIt4*F5`+0V}02p*Cv={BWr2DXnNv80+}H1=Yxm% zapV1Pdgk0`=^&`ZlTH{l_;J=YP`GVH!rb%5W1{?RtF<;D ziq&64=HFv2wrCQ1uds2A<0BMzwR10S8WCalxYw@1kLta6XdWxAskO;<-2!Eq_}+(_*36uj&=Jg3~aT&sZCvSZKAwHL%a2Qp!J{ zDVW4ONL_gBGH`p`-MsIE*d0?{!{5f- z+H1p_%yCTx6CF9>nW77SsF6yesTFJ_4M*+*tqC-hst7crmdQ zZxnc~(4cxl5;p>u_mn*=I%HoNnOxzBrit&M@G#cw#0k3r{c_n)*el7p2M}@W_j?qt zVy%Aj>&gx0tMRcs&L*?W)KS2Ygs46GnRA}40_9k#pim1)Z9;(pkfo9|9vlXG#*bx+~|eY$+>+nh62nhmg)7V(#9oUSge|dN8{U4tV zuo&jB)TrqWs#`Qh*qxJUJfNibdeGa}26!%RQn*k;u3LvJ7<^F8^--ZP6O5czA9c)^ zEI#4*W&S#NZ6JLO{xiUJU?$v;>|O`nk$uv32w_t}NhrnIdB$CnbF(sG?#3?1cjP=y!v|79bV0CL;N;ySZnPT37M$v3*$%kasOV%c*XZ27 zFz`jm6lx=2=xI_GPSOHu@)HHI z$1{i6j79{Gs&XM$Tu41^&{!`0BnRSt*__`oYG~|m%$Gqeo*5ygiV(Bg=O4OAG2yH) zKiv#>SzNy2Un4#W+5n49(#=IGKNUJGI88E7EW6y^*|=D;@R5+p+sx}2F8ct)tF_s8 z+xo#>U}*1VZ6qSWx~=hru}meg6g7 z6gf|{jjsXgMSsy3l7d10v1?#zAwp&ljt!12jvtKmUknkV#-r4CbhU*FiNjzL!Xhvc zNl76I7y?xPPZI$MjQ|z>jq-s2oWE1nzY5_0GKgfve=ru~X11jdi9R4EGgUTzm~Z`m z3}VH+`a-AIe5=gO?TR&wnIQ}Tf|5FqW>Y|RW=*>Tro9v7pv&1$C@=b|nTO;wo8a?H z>XSi3IVS?lXVY3GOM9hUU34YQ205W(=}6Twx1?;w1K0%1%hDHNUmw5ET754jbZ30T z0gY9>7pQufuVLW{`NIMyT%pvgQ-^#^olI{KQ4!BG$%e!*E4kyw1~+@NKgBb0R+|bd zOwe1T8lxGhdtHo)mRY({?&VE|W`L{6m9pL$sMIk!x}^+Dd^HEU*GU!*-E3O9%`|2=?rgDaQMG8qjXp~{RR z0W7>o^E5N$Pl;m)xl*KMnd8qo_9>Z(CnvGa0>>|Y|u)Z4^4CyVgGjx@{ z6}gjoVLx294W7--z^m_^_CJYMj|tXx7*q5X@xYSt5Pv{MhfV5~_5+ocXIoQwBZ{(n zXKB4I{82$D!&h;!Ss&TNvMT#ehE^IDP%BYws%LynRViHJHSEcuf;t00r!H1iRn92n z)Fo8d$<##vTxVKbAAd&r;I*5hq)VU*iz_POT}l9p*v;0T0j8s(q|*yN9??kOA47i2 zRQ@{$(S2}8O(-$7Rg!%b03MV4!+Ykd9UwQhWcp_xZYiti-R&k|e@~8?o$jzlHVoxH zcUd#_SgLt{gxbTR{dU5lRX#}v+X&YvdZZn^7cvzS{cbmGvCsETwL|0HuP2s6w)`SR z-sR*S0(vV{_sqA(iUYXm9*uh5UdyQ~ta@r6UK-KX$=+9Nfq%?RI63l;P@Xp`a4F$} z>^JRaHrGl4;wK-JW%>V@(Kt}8+DJ!BSC4(QNm68-HJn(uw$Q&->NmqODDY|f7=BAi z$o_OuZ-Y>#pQE`o!Y}U{e4m-@yHURrSkj30c~9y4Icn^HFym|9HPMSBy8^vzbsmo~ zD@QeISLwUtf@)Y-rCRfE|H`}ra{lPZ1!G^K2THD>o$y6z*1;1N;bfmaKV7pIflN+e z3f4uv$b{-I+5JWas;tob!N!OnV?zU;{t!vPmR;-pf`MTdg?*~d##oO7eF~wx<;Gr% zaGo>xY%0DjHyCYJE9zhKS-sU?-B&M;w)3n{kyB7)auuJJ{1Rdo99 z06Ermrr;t&7qR$D}`y~xDd6Dc{00E=^*35rXp%uT#=BnWVVKbt^zT~kvu zbk2{mmjllPEOXp8#Nl1@3eP-5wh2;^+xat2*;;lP8gF=A4vi<-p>qN$x!XG|PN;8_ zD|!!po#|IUTOmI09>px;dkk53(}vwVw71}WL2{}PM!sYUO}2zYlBu3Qz|ygAtOsuw z2ei|Cy(0(gbf;5wJMiic)hRxqAwLWs-mib2so`Fj^*S}EM`Vdnkai&Kb?s^o)3&_!^Ve75#DT{Pl!kWVVORifG)F#zYdx1^J?Yj(WV~GdHEG z^8H4JOz;g!3PuOX{hl#FhK$!7wT-V;=~>SiU`ZN=Lpe*>43%He2F5>Wbed&&ph1#2 z8L30(xicITv2*8xTPLD&C_F!8ho-QkriY^aU^*}%7|B0>1|R?*2M>z;&K#*1-R3uD zeyX)TITNIM`I;jW;TT|!14Hj>RafFsOZ6Qj;$mdy@{->(3F^WN4(2uY|La8k2NbVW Ag#Z8m literal 0 HcmV?d00001 diff --git a/server/api-service/lowcoder-server/pom.xml b/server/api-service/lowcoder-server/pom.xml index cd2d2ed86..5021d2b61 100644 --- a/server/api-service/lowcoder-server/pom.xml +++ b/server/api-service/lowcoder-server/pom.xml @@ -1,281 +1,380 @@ - - 4.0.0 - - lowcoder-root - org.lowcoder - ${revision} - + + 4.0.0 + + lowcoder-root + org.lowcoder + ${revision} + - lowcoder-server - jar + lowcoder-server + jar - lowcoder-server + lowcoder-server - - 17 - false - ${skipTests} - ${skipTests} - + + UTF-8 + UTF-8 - + 17 - - org.lowcoder - lowcoder-sdk - - - org.lowcoder - lowcoder-infra - - - org.lowcoder - lowcoder-domain - + false + ${skipTests} + ${skipTests} - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security - spring-security-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - 2.2.0 - - - io.projectreactor.tools - blockhound - - - org.springframework.boot - spring-boot-starter-data-mongodb-reactive - + cert/signing.p12 + pkcs12 + dev + lowcoder + ${keystore.password} + ${keystore.password} + - - org.springframework.boot - spring-boot-starter-data-redis-reactive - + - - org.projectlombok - lombok - + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder.plugin + lowcoder-plugin-api + - com.google.guava - guava - - - commons-io - commons-io - - - org.springframework.boot - spring-boot-starter-actuator - - - io.micrometer - micrometer-registry-prometheus - - - io.sentry - sentry-spring-boot-starter - - - org.apache.httpcomponents - httpclient - - - org.apache.commons - commons-text - - - - org.apache.commons - commons-collections4 - - + + org.apache.commons + commons-collections4 + + + - - io.netty - netty-all - runtime - - - io.projectreactor - reactor-tools - - - org.mockito - mockito-inline - test - - - org.mockito - mockito-core - test - - - junit - junit - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - io.projectreactor - reactor-test - test - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - test - - - com.jayway.jsonpath - json-path - - - jakarta.servlet - jakarta.servlet-api - + + io.netty + netty-all + runtime + + + io.projectreactor + reactor-tools + + + org.mockito + mockito-inline + test + + + org.mockito + mockito-core + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + test + + + com.jayway.jsonpath + json-path + + + jakarta.servlet + jakarta.servlet-api + - - com.auth0 - java-jwt - 4.4.0 - + + com.auth0 + java-jwt + 4.4.0 + - - it.ozimov - embedded-redis - 0.7.3 - test - - - org.apache.directory.server - apacheds-test-framework - 2.0.0.AM26 - test - - - org.junit.vintage - junit-vintage-engine - 5.9.3 - test - - - io.jsonwebtoken - jjwt-api - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - + + org.passay + passay + 1.6.3 + + + + it.ozimov + embedded-redis + 0.7.3 + test + + + org.apache.directory.server + apacheds-test-framework + 2.0.0.AM26 + test + + + org.junit.vintage + junit-vintage-engine + 5.9.3 + test + + + io.jsonwebtoken + jjwt-api + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + org.springframework + spring-aspects + + + org.springframework + spring-aspects + + + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + - + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.lowcoder.api.ServerApplication + true + true + true + + + + + + org.apache.maven.plugins + maven-jarsigner-plugin + 3.0.0 + + + sign + + sign + + + + verify + + verify + + + + + ${keystore.type} + ${keystore.path} + ${keystore.alias} + ${keystore.store.password} + ${keystore.key.password} + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.2 - - ${skipUnitTests} - - **/*IntegrationTest.java - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${skipIntegrationTests} - - **/*IntegrationTest.java - - - -Dpf4j.pluginsDir=../lowcoder-plugins/plugins - - - - - - integration-test - verify - - - - - - maven-antrun-plugin - - - copy-plugins-jar-for-integration-tests - pre-integration-test - - - - - - - - - - run - - - - delete-plugins-after-integration-tests-phase - post-integration-test - - - - - - - run - - - - - - + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + ${skipUnitTests} + + **/*IntegrationTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipIntegrationTests} + + **/*IntegrationTest.java + + + -Dpf4j.pluginsDir=../lowcoder-plugins/plugins + + + + + + integration-test + verify + + + + + + maven-antrun-plugin + + + copy-plugins-jar-for-integration-tests + pre-integration-test + + + + + + + + + + run + + + + delete-plugins-after-integration-tests-phase + post-integration-test + + + + + + + run + + + + + + diff --git a/server/api-service/lowcoder-server/src/main/assembly/assembly.xml b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml new file mode 100644 index 000000000..b2f6bb420 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml @@ -0,0 +1,58 @@ + + + lowcoder-dist + + dir + + + true + lowcoder + + + + target/${project.artifactId}-${project.version}.jar + + application.jar + + + + + + + \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java index 3a442255b..09c94ee06 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java @@ -45,6 +45,9 @@ public void init() { public static void main(String[] args) { + /** Disable Java Flight Recorder for Redis Lettuce driver **/ + System.setProperty("io.lettuce.core.jfr", "false"); + Schedulers.enableMetrics(); new SpringApplicationBuilder(ServerApplication.class) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index d12297b33..61f9a79ca 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -1,11 +1,12 @@ package org.lowcoder.api.application; import static org.apache.commons.collections4.SetUtils.emptyIfNull; -import static org.lowcoder.infra.event.EventType.APPLICATION_CREATE; -import static org.lowcoder.infra.event.EventType.APPLICATION_DELETE; -import static org.lowcoder.infra.event.EventType.APPLICATION_RECYCLED; -import static org.lowcoder.infra.event.EventType.APPLICATION_RESTORE; -import static org.lowcoder.infra.event.EventType.APPLICATION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_CREATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RECYCLED; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RESTORE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_VIEW; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -93,12 +94,11 @@ public Mono> getEditingApplication(@PathVariable S .map(ResponseView::success); } - // will call the check in ApplicationApiService and ApplicationService @Override public Mono> getPublishedApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_ALL) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 166801e7d..c28740cdc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -149,7 +149,7 @@ public Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, boolean createWorkspace = authUser.getOrgId() == null && StringUtils.isBlank(invitationId) && authProperties.getWorkspaceCreation(); if (user.getIsNewUser() && createWorkspace) { - return onUserRegister(user); + return onUserRegister(user, false); } return Mono.empty(); }) @@ -166,7 +166,7 @@ public Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, .then(businessEventPublisher.publishUserLoginEvent(authUser.getSource())); } - private Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { + public Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { if(linkExistingUser) { return sessionUserService.getVisitor() @@ -256,8 +256,8 @@ protected Connection getAuthConnection(AuthUser authUser, User user) { .get(); } - protected Mono onUserRegister(User user) { - return organizationService.createDefault(user).then(); + public Mono onUserRegister(User user, boolean isSuperAdmin) { + return organizationService.createDefault(user, isSuperAdmin).then(); } protected Mono onUserLogin(String orgId, User user, String source) { @@ -362,7 +362,7 @@ private Mono removeTokensByAuthId(String authId) { private Mono checkIfAdmin() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.empty(); } return deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 1494f7786..1cbfeef9a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java @@ -1,11 +1,11 @@ package org.lowcoder.api.datasource; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_CREATE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_DELETE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_DELETE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_GRANT; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_UPDATE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_CREATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_GRANT; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_UPDATE; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; import static org.lowcoder.sdk.util.LocaleUtils.getLocale; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java index 1170b9761..763dccd7c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java @@ -1,7 +1,10 @@ package org.lowcoder.api.framework.configuration; +import org.lowcoder.api.ServerApplication; import org.lowcoder.sdk.config.CommonConfig; +import org.pf4j.spring.SpringPluginManager; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.system.ApplicationHome; import org.springframework.boot.web.servlet.MultipartConfigFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,6 +18,18 @@ public class ApplicationConfiguration @Autowired private CommonConfig common; + @Bean("applicationHome") + public ApplicationHome applicatioHome() + { + return new ApplicationHome(ServerApplication.class); + } + + @Bean + public SpringPluginManager pluginManager() + { + return new SpringPluginManager(); + } + @Bean public MultipartConfigElement multipartConfigElement() { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java new file mode 100644 index 000000000..d57b0ab1d --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java @@ -0,0 +1,16 @@ +package org.lowcoder.api.framework.configuration; + +import org.lowcoder.api.framework.plugin.endpoint.ReloadableRouterFunctionMapping; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.WebFluxConfigurationSupport; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; + +@Configuration +public class CustomWebFluxConfigurationSupport extends WebFluxConfigurationSupport +{ + @Override + protected RouterFunctionMapping createRouterFunctionMapping() + { + return new ReloadableRouterFunctionMapping(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java new file mode 100644 index 000000000..a5d9df955 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java @@ -0,0 +1,58 @@ +package org.lowcoder.api.framework.configuration; + +import java.util.ArrayList; + +import org.lowcoder.api.framework.plugin.LowcoderPluginManager; +import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler; +import org.lowcoder.api.framework.plugin.security.PluginAuthorizationManager; +import org.lowcoder.plugin.api.EndpointExtension; +import org.springframework.aop.Advisor; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import reactor.core.publisher.Mono; + + +@Configuration +public class PluginConfiguration +{ + + @SuppressWarnings("unchecked") + @Bean + @DependsOn("lowcoderPluginManager") + RouterFunction pluginEndpoints(LowcoderPluginManager pluginManager, PluginEndpointHandler pluginEndpointHandler) + { + RouterFunction pluginsList = RouterFunctions.route() + .GET(RequestPredicates.path(PluginEndpointHandler.PLUGINS_BASE_URL), req -> ServerResponse.ok().body(Mono.just(pluginManager.getLoadedPluginsInfo()), ArrayList.class)) + .build(); + + RouterFunction endpoints = pluginEndpointHandler.registeredEndpoints().stream() + .map(r-> (RouterFunction)r) + .reduce((o, r )-> (RouterFunction) o.andOther(r)) + .orElse(null); + + return (endpoints == null) ? pluginsList : pluginsList.andOther(endpoints); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor protectPluginEndpoints(PluginAuthorizationManager pluginAauthManager) + { + AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(EndpointExtension.class, true); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pointcut, pluginAauthManager); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() -1); + return interceptor; + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java new file mode 100644 index 000000000..6f45c7e7c --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java @@ -0,0 +1,38 @@ +package org.lowcoder.api.framework.filter; + +import org.lowcoder.infra.config.repository.ServerConfigRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import static org.lowcoder.api.framework.filter.FilterOrder.API_DELAY_FILTER; + +@Component +public class APIDelayFilter implements WebFilter, Ordered { + + @Autowired + private ServerConfigRepository serverConfigRepository; + + @Override + public int getOrder() { + return API_DELAY_FILTER.getOrder(); + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return serverConfigRepository.findByKey("isRateLimited") + .map(serverConfig -> { + if(serverConfig.getValue() != null && Boolean.parseBoolean(serverConfig.getValue().toString())) { + return Mono.delay(Duration.ofSeconds(5)).block(); + } else { + return Mono.empty(); + } + }).then(chain.filter(exchange)); + } +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java index 8e8c0d9be..9bf6b4100 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java @@ -10,6 +10,8 @@ public enum FilterOrder { REQUEST_COST(BEFORE_PROXY_CHAIN), THROTTLING(BEFORE_PROXY_CHAIN), + API_DELAY_FILTER(BEFORE_PROXY_CHAIN), + // WEB_FILTER_CHAIN_PROXY here USER_BAN(AFTER_PROXY_CHAIN), diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java new file mode 100644 index 000000000..e8c2fb765 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java @@ -0,0 +1,18 @@ +package org.lowcoder.api.framework.filter; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +@Configuration +public class ReactiveRequestContextFilter implements WebFilter { + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + return chain.filter(exchange) + .contextWrite(ctx -> ctx.put(ReactiveRequestContextHolder.SERVER_HTTP_REQUEST, request)); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java new file mode 100644 index 000000000..98477a012 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java @@ -0,0 +1,13 @@ +package org.lowcoder.api.framework.filter; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import reactor.core.publisher.Mono; + +public class ReactiveRequestContextHolder { + public static final Class SERVER_HTTP_REQUEST = ServerHttpRequest.class; + + public static Mono getRequest() { + return Mono.subscriberContext() + .map(ctx -> ctx.get(SERVER_HTTP_REQUEST)); + } +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java index e3e8ba138..edbf45c9f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java @@ -48,7 +48,7 @@ public class ThrottlingFilter implements WebFilter, Ordered { @PostConstruct private void init() { urlRateLimiter = configCenter.threshold().ofMap("urlRateLimiter", String.class, Integer.class, emptyMap()); - log.info("API rate limit filter enabled with default rate limit set to: {} requests per second"); + log.info("API rate limit filter enabled with default rate limit set to: {} requests per second", defaultApiRateLimit); } @Nonnull diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java new file mode 100644 index 000000000..e4107919f --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java @@ -0,0 +1,130 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.plugin.api.LowcoderServices; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Component +@Slf4j +public class LowcoderPluginManager +{ + private final LowcoderServices lowcoderServices; + private final PluginLoader pluginLoader; + private final Environment environment; + + private Map plugins = new LinkedHashMap<>(); + + @PostConstruct + private void loadPlugins() + { + registerPlugins(); + List sorted = new ArrayList<>(plugins.values()); + sorted.sort(Comparator.comparing(LowcoderPlugin::loadOrder)); + + for (LowcoderPlugin plugin : sorted) + { + PluginExecutor executor = new PluginExecutor(plugin, getPluginEnvironmentVariables(plugin), lowcoderServices); + executor.start(); + } + } + + @PreDestroy + public void unloadPlugins() + { + for (LowcoderPlugin plugin : plugins.values()) + { + try + { + plugin.unload(); + } + catch(Throwable cause) + { + log.warn("Error unloading plugin: {}!", plugin.pluginId(), cause); + } + } + } + + public List getLoadedPluginsInfo() + { + List infos = new ArrayList<>(); + for (LowcoderPlugin plugin : plugins.values()) + { + infos.add(new PluginInfo(plugin.pluginId(), plugin.description(), plugin.pluginInfo())); + } + return infos; + } + + private Map getPluginEnvironmentVariables(LowcoderPlugin plugin) + { + Map env = new HashMap<>(); + + String varPrefix = "PLUGIN_" + plugin.pluginId().toUpperCase().replaceAll("-", "_") + "_"; + MutablePropertySources propertySources = ((AbstractEnvironment) environment).getPropertySources(); + List properties = StreamSupport.stream(propertySources.spliterator(), false) + .filter(propertySource -> propertySource instanceof EnumerablePropertySource) + .map(propertySource -> ((EnumerablePropertySource) propertySource).getPropertyNames()) + .flatMap(Arrays:: stream) + .distinct() + .sorted() + .filter(prop -> prop.startsWith(varPrefix)) + .collect(Collectors.toList()); + + for (String prop : properties) + { + env.put(StringUtils.removeStart(prop, varPrefix), environment.getProperty(prop)); + } + + return env; + } + + private void registerPlugins() + { + List loaded = pluginLoader.loadPlugins(); + if (CollectionUtils.isNotEmpty(loaded)) + { + for (LowcoderPlugin plugin : loaded) + { + if (!plugins.containsKey(plugin.pluginId())) + { + log.info("Registered plugin: {} ({})", plugin.pluginId(), plugin.getClass().getName()); + plugins.put(plugin.pluginId(), plugin); + } + else + { + log.warn("Plugin {} already registered (from: {}), skipping {}.", plugin.pluginId(), + plugins.get(plugin.pluginId()).getClass().getName(), + plugin.getClass().getName()); + } + } + } + } + + private record PluginInfo( + String id, + String description, + Object info + ) {} + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java new file mode 100644 index 000000000..ddd66ba3f --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java @@ -0,0 +1,140 @@ +package org.lowcoder.api.framework.plugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.boot.system.ApplicationHome; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +@Component +public class PathBasedPluginLoader implements PluginLoader +{ + private final CommonConfig common; + private final ApplicationHome applicationHome; + + @Override + public List loadPlugins() + { + List plugins = new ArrayList<>(); + + List pluginJars = findPluginsJars(); + if (pluginJars.isEmpty()) + { + return plugins; + } + + for (String pluginJar : pluginJars) + { + log.debug("Inspecting plugin jar candidate: {}", pluginJar); + List loadedPlugins = loadPluginCandidates(pluginJar); + if (loadedPlugins.isEmpty()) + { + log.debug(" - no plugins found in the jar file"); + } + else + { + for (LowcoderPlugin plugin : loadedPlugins) + { + plugins.add(plugin); + } + } + } + + return plugins; + } + + protected List findPluginsJars() + { + List candidates = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(common.getPluginDirs())) + { + for (String pluginDir : common.getPluginDirs()) + { + final Path pluginPath = getAbsoluteNormalizedPath(pluginDir); + if (pluginPath != null) + { + candidates.addAll(findPluginCandidates(pluginPath)); + } + } + } + + return candidates; + } + + + protected List findPluginCandidates(Path pluginsDir) + { + List pluginCandidates = new ArrayList<>(); + try + { + Files.walk(pluginsDir) + .filter(Files::isRegularFile) + .filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar")) + .forEach(path -> pluginCandidates.add(path.toString())); + } + catch(IOException cause) + { + log.error("Error walking plugin folder! - {}", cause.getMessage()); + } + + return pluginCandidates; + } + + protected List loadPluginCandidates(String pluginJar) + { + List pluginCandidates = new ArrayList<>(); + + try + { + Path pluginPath = Path.of(pluginJar); + PluginClassLoader pluginClassLoader = new PluginClassLoader(pluginPath.getFileName().toString(), pluginPath); + + ServiceLoader pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader); + if (pluginServices != null ) + { + Iterator pluginIterator = pluginServices.iterator(); + while(pluginIterator.hasNext()) + { + LowcoderPlugin plugin = pluginIterator.next(); + log.debug(" - loaded plugin: {} - {}", plugin.pluginId(), plugin.description()); + pluginCandidates.add(plugin); + } + } + } + catch(Throwable cause) + { + log.warn("Error loading plugin!", cause); + } + + return pluginCandidates; + } + + private Path getAbsoluteNormalizedPath(String path) + { + if (StringUtils.isNotBlank(path)) + { + Path absPath = Path.of(path); + if (!absPath.isAbsolute()) + { + absPath = Path.of(applicationHome.getDir().getAbsolutePath(), absPath.toString()); + } + return absPath.normalize().toAbsolutePath(); + } + + return null; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java new file mode 100644 index 000000000..e0be8dac2 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java @@ -0,0 +1,104 @@ +package org.lowcoder.api.framework.plugin; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +public class PluginClassLoader extends URLClassLoader +{ + private static final ClassLoader baseClassLoader = ClassLoader.getPlatformClassLoader(); + private final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + + public PluginClassLoader(String name, Path pluginPath) + { + super(name, pathToURLs(pluginPath), baseClassLoader); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException + { + Class clazz = findLoadedClass(name); + if (clazz != null) + { + return clazz; + } + + if (name.startsWith("org.lowcoder.plugin.api.")) + { + try + { + clazz = appClassLoader.loadClass(name); + return clazz; + } + catch(Throwable cause) + { + log.error("[{}] :: Error loading class with appClassLoader - {}", name, cause.getMessage(), cause ); + } + } + + + try + { + clazz = super.loadClass(name, resolve); + if (clazz != null) + { + return clazz; + } + } + catch(NoClassDefFoundError cause) + { + log.error("[{}] :: Error loading class - {}", name, cause.getMessage(), cause ); + } + + return null; + } + + @Override + public URL getResource(String name) { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + { + return appClassLoader.getResource(name); + } + return super.getResource(name); + } + + + @Override + public Enumeration getResources(String name) throws IOException + { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + { + return appClassLoader.getResources(name); + } + return super.getResources(name); + } + + private static URL[] pathToURLs(Path path) + { + URL[] urls = null; + try + { + urls = new URL[] { path.toUri().toURL() }; + } + catch(MalformedURLException cause) + { + /** should not happen **/ + } + + return urls; + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java new file mode 100644 index 000000000..bbce19994 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java @@ -0,0 +1,36 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.Map; + +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.plugin.api.LowcoderServices; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PluginExecutor extends Thread +{ + private Map env; + private LowcoderPlugin plugin; + private LowcoderServices services; + + public PluginExecutor(LowcoderPlugin plugin, Map env, LowcoderServices services) + { + this.env = env; + this.plugin = plugin; + this.services = services; + this.setContextClassLoader(plugin.getClass().getClassLoader()); + this.setName(plugin.pluginId()); + } + + @Override + public void run() + { + if (plugin.load(env, services)) + { + log.info("Plugin [{}] loaded and running.", plugin.pluginId()); + } + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java new file mode 100644 index 000000000..25ed33eb4 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java @@ -0,0 +1,11 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.List; + +import org.lowcoder.plugin.api.LowcoderPlugin; + +public interface PluginLoader +{ + List loadPlugins(); + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java new file mode 100644 index 000000000..1cd455e20 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java @@ -0,0 +1,59 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler; +import org.lowcoder.infra.config.repository.ServerConfigRepository; +import org.lowcoder.plugin.api.LowcoderServices; +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.event.LowcoderEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Component +public class SharedPluginServices implements LowcoderServices +{ + private final PluginEndpointHandler pluginEndpointHandler; + + @Autowired + private ServerConfigRepository serverConfigRepository; + + private List> eventListeners = new LinkedList<>(); + + @Override + public void registerEventListener(Consumer listener) + { + this.eventListeners.add(listener); + } + + @EventListener(classes = LowcoderEvent.class) + private void publishEvents(LowcoderEvent event) + { + for (Consumer listener : eventListeners) + { + listener.accept(event); + } + } + + @Override + public void registerEndpoints(String urlPrefix, List endpoints) + { + pluginEndpointHandler.registerEndpoints(urlPrefix, endpoints); + } + + @Override + public void setConfig(String key, Object value) { + serverConfigRepository.upsert(key, value).block(); + } + + @Override + public Object getConfig(String key) { + return serverConfigRepository.findByKey(key).block(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java new file mode 100644 index 000000000..aa75bdc17 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java @@ -0,0 +1,198 @@ +package org.lowcoder.api.framework.plugin.data; + +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.PluginEndpoint.Method; +import org.lowcoder.plugin.api.data.EndpointRequest; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.server.ServerRequest; + +import java.net.URI; +import java.security.Principal; +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +public class PluginServerRequest implements EndpointRequest +{ + private URI uri; + private PluginEndpoint.Method method; + private CompletableFuture body; + private Map> headers; + private Map>> cookies; + private Map attributes; + private Map pathVariables; + + private Map> queryParams; + private CompletableFuture principal; + + + public PluginServerRequest() + { + headers = new HashMap<>(); + cookies = new HashMap<>(); + attributes = new HashMap<>(); + pathVariables = new HashMap<>(); + queryParams = new HashMap<>(); + } + + public static PluginServerRequest fromServerRequest(ServerRequest request) + { + PluginServerRequest psr = new PluginServerRequest(); + + psr.uri = request.uri(); + psr.method = fromHttpMetod(request.method()); + psr.body = request.bodyToMono(byte[].class).toFuture(); + + if (request.headers() != null) + { + HttpHeaders httpHeaders = request.headers().asHttpHeaders(); + psr.headers = httpHeaders; + } + + if (request.cookies() != null) + { + request.cookies().entrySet().stream() + .forEach(entry -> { + psr.cookies.put(entry.getKey(), fromHttpCookieList(entry.getValue())); + }); + } + + if (request.attributes() != null) + { + request.attributes().forEach((name, value) -> { + psr.attributes.put(name, value); + }); + } + + if (request.pathVariables() != null) + { + request.pathVariables().entrySet() + .forEach(entry -> { + psr.pathVariables.put(entry.getKey(), entry.getValue()); + }); + } + + if (request.queryParams() != null) + { + request.queryParams().entrySet() + .forEach(entry -> { + psr.queryParams.put(entry.getKey(), entry.getValue()); + }); + } + + psr.principal = request.principal().toFuture(); + + return psr; + } + + private static List> fromHttpCookieList(List cookies) + { + List> list = new LinkedList<>(); + + if (cookies != null) + { + cookies.stream() + .forEach(cookie -> { + list.add(new SimpleEntry(cookie.getName(), cookie.getValue())); + }); + } + + return list; + } + + + + @Override + public URI uri() { + return uri; + } + @Override + public Method method() { + return method; + } + @Override + public CompletableFuture body() { + return body; + } + @Override + public Map> headers() { + return headers; + } + @Override + public Map>> cookies() { + return cookies; + } + @Override + public Map attributes() { + return attributes; + } + @Override + public Map pathVariables() { + return pathVariables; + } + + @Override + public Map> queryParams() { + return queryParams; + } + @Override + public CompletableFuture principal() { + return principal; + } + + + public static HttpMethod fromPluginEndpointMethod(PluginEndpoint.Method method) + { + switch(method) + { + case GET: + return HttpMethod.GET; + case POST: + return HttpMethod.POST; + case PUT: + return HttpMethod.PUT; + case PATCH: + return HttpMethod.PATCH; + case DELETE: + return HttpMethod.DELETE; + case OPTIONS: + return HttpMethod.OPTIONS; + } + return null; + } + + public static PluginEndpoint.Method fromHttpMetod(HttpMethod method) + { + if (method == HttpMethod.GET) + { + return PluginEndpoint.Method.GET; + } + else if (method == HttpMethod.POST) + { + return PluginEndpoint.Method.POST; + } + else if (method == HttpMethod.PUT) + { + return PluginEndpoint.Method.PUT; + } + else if (method == HttpMethod.PATCH) + { + return PluginEndpoint.Method.PATCH; + } + else if (method == HttpMethod.DELETE) + { + return PluginEndpoint.Method.DELETE; + } + else if (method == HttpMethod.OPTIONS) + { + return PluginEndpoint.Method.OPTIONS; + } + return null; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java new file mode 100644 index 000000000..11922c3dd --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java @@ -0,0 +1,15 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import java.util.List; + +import org.lowcoder.plugin.api.PluginEndpoint; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +public interface PluginEndpointHandler +{ + public static final String PLUGINS_BASE_URL = "/api/plugins/"; + + void registerEndpoints(String urlPrefix, List endpoints); + List> registeredEndpoints(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java new file mode 100644 index 000000000..bcee69580 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -0,0 +1,195 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.OPTIONS; +import static org.springframework.web.reactive.function.server.RequestPredicates.PATCH; +import static org.springframework.web.reactive.function.server.RequestPredicates.POST; +import static org.springframework.web.reactive.function.server.RequestPredicates.PUT; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.api.framework.plugin.data.PluginServerRequest; +import org.lowcoder.plugin.api.EndpointExtension; +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.data.EndpointRequest; +import org.lowcoder.plugin.api.data.EndpointResponse; +import org.lowcoder.sdk.exception.BaseException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseCookie; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@Component +public class PluginEndpointHandlerImpl implements PluginEndpointHandler +{ + private List> routes = new ArrayList<>(); + + private final ApplicationContext applicationContext; + private final DefaultListableBeanFactory beanFactory; + + @Override + public void registerEndpoints(String pluginUrlPrefix, List endpoints) + { + String urlPrefix = PLUGINS_BASE_URL + pluginUrlPrefix; + + if (CollectionUtils.isNotEmpty(endpoints)) + { + for (PluginEndpoint endpoint : endpoints) + { + Method[] handlers = endpoint.getClass().getDeclaredMethods(); + if (handlers != null && handlers.length > 0) + { + for (Method handler : handlers) + { + registerEndpointHandler(urlPrefix, endpoint, handler); + } + } + } + + ((ReloadableRouterFunctionMapping)beanFactory.getBean("routerFunctionMapping")).reloadFunctionMappings(); + } + } + + @Override + public List> registeredEndpoints() + { + return routes; + } + + private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler) + { + if (handler.isAnnotationPresent(EndpointExtension.class)) + { + if (checkHandlerMethod(handler)) + { + + EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); + String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); + + RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> + { + Mono result = null; + try + { + EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(req)); + result = createServerResponse(response); + } + catch (IllegalAccessException | InvocationTargetException cause) + { + throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); + } + return result; + }); + routes.add(routerFunction); + registerRouterFunctionMapping(endpointName, routerFunction); + + log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); + } + else + { + log.error("Cannot register plugin endpoint: {} -> {}! Handler method must be defined as: public Mono {}(ServerRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); + } + } + } + + private void registerRouterFunctionMapping(String endpointName, RouterFunction routerFunction) + { + String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); + + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> { + return routerFunction; + }); + + log.debug("Registering RouterFunction bean definition: {}", beanName); + } + + + private Mono createServerResponse(EndpointResponse pluginResponse) + { + /** Create response with given status **/ + BodyBuilder builder = ServerResponse.status(pluginResponse.statusCode()); + + /** Set response headers **/ + if (pluginResponse.headers() != null && !pluginResponse.headers().isEmpty()) + { + pluginResponse.headers().entrySet() + .forEach(entry -> builder.header(entry.getKey(), entry.getValue().toArray(new String[] {}))); + } + + /** Set cookies if available **/ + if (pluginResponse.cookies() != null && !pluginResponse.cookies().isEmpty()) + { + pluginResponse.cookies().values() + .forEach(cookies -> cookies + .forEach(cookie -> builder + .cookie(ResponseCookie.from(cookie.getKey(), cookie.getValue()).build()))); + } + + /** Set response body if available **/ + if (pluginResponse.body() != null) + { + return builder.bodyValue(pluginResponse.body()); + } + + return builder.build(); + } + + private boolean checkHandlerMethod(Method method) + { + ResolvableType returnType = ResolvableType.forMethodReturnType(method); + + return (returnType.getRawClass().isAssignableFrom(EndpointResponse.class) + && method.getParameterCount() == 1 + && method.getParameterTypes()[0].isAssignableFrom(EndpointRequest.class) + ); + } + + private RequestPredicate createRequestPredicate(String basePath, EndpointExtension endpoint) + { + switch(endpoint.method()) + { + case GET: + return GET(pluginEndpointUri(basePath, endpoint.uri())); + case POST: + return POST(pluginEndpointUri(basePath, endpoint.uri())); + case PUT: + return PUT(pluginEndpointUri(basePath, endpoint.uri())); + case PATCH: + return PATCH(pluginEndpointUri(basePath, endpoint.uri())); + case DELETE: + return DELETE(pluginEndpointUri(basePath, endpoint.uri())); + case OPTIONS: + return OPTIONS(pluginEndpointUri(basePath, endpoint.uri())); + } + return null; + } + + private String pluginEndpointUri(String basePath, String uri) + { + return StringUtils.join(basePath, StringUtils.prependIfMissing(uri, "/")); + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java new file mode 100644 index 000000000..42e8e5690 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java @@ -0,0 +1,20 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; + + +public class ReloadableRouterFunctionMapping extends RouterFunctionMapping +{ + /** + * Rescan application context for RouterFunction beans + */ + public void reloadFunctionMappings() + { + initRouterFunctions(); + if (getRouterFunction() != null) + { + RouterFunctions.changeParser(getRouterFunction(), getPathPatternParser()); + } + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java new file mode 100644 index 000000000..e1849c444 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -0,0 +1,94 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.EndpointExtension; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@Component +public class PluginAuthorizationManager implements ReactiveAuthorizationManager +{ + private final MethodSecurityExpressionHandler expressionHandler; + + public PluginAuthorizationManager() + { + this.expressionHandler = new DefaultMethodSecurityExpressionHandler(); + } + + @Override + public Mono check(Mono authentication, MethodInvocation invocation) + { + log.info(" invocation :: {}", invocation.getMethod()); + + Method method = invocation.getMethod(); + EndpointExtension endpointExtension = AnnotationUtils.findAnnotation(method, EndpointExtension.class); + if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize())) + { + return Mono.empty(); + } + + Expression authorizeExpression = this.expressionHandler.getExpressionParser() + .parseExpression(endpointExtension.authorize()); + + return authentication + .map(auth -> expressionHandler.createEvaluationContext(auth, invocation)) + .flatMap(ctx -> evaluateAsBoolean(authorizeExpression, ctx)) + .map(granted -> new ExpressionAuthorizationDecision(granted, authorizeExpression)); + } + + + private Mono evaluateAsBoolean(Expression expr, EvaluationContext ctx) + { + return Mono.defer(() -> + { + Object value; + try + { + value = expr.getValue(ctx); + } + catch (EvaluationException ex) + { + return Mono.error(() -> new IllegalArgumentException( + "Failed to evaluate expression '" + expr.getExpressionString() + "'", ex)); + } + + if (value instanceof Boolean bool) + { + return Mono.just(bool); + } + + if (value instanceof Mono monoBool) + { + Mono monoValue = monoBool; + return monoValue + .filter(Boolean.class::isInstance) + .map(Boolean.class::cast) + .switchIfEmpty(createInvalidReturnTypeMono(expr)); + } + return createInvalidReturnTypeMono(expr); + }); + } + + private static Mono createInvalidReturnTypeMono(Expression expr) + { + return Mono.error(() -> new IllegalStateException( + "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index b933a63e1..555c0a64b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -1,6 +1,24 @@ package org.lowcoder.api.framework.security; +import static org.lowcoder.infra.constant.NewUrl.GITHUB_STAR; +import static org.lowcoder.infra.constant.Url.APPLICATION_URL; +import static org.lowcoder.infra.constant.Url.CONFIG_URL; +import static org.lowcoder.infra.constant.Url.CUSTOM_AUTH; +import static org.lowcoder.infra.constant.Url.DATASOURCE_URL; +import static org.lowcoder.infra.constant.Url.GROUP_URL; +import static org.lowcoder.infra.constant.Url.INVITATION_URL; +import static org.lowcoder.infra.constant.Url.ORGANIZATION_URL; +import static org.lowcoder.infra.constant.Url.QUERY_URL; +import static org.lowcoder.infra.constant.Url.STATE_URL; +import static org.lowcoder.infra.constant.Url.USER_URL; +import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER; +import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER_ID; + +import java.util.List; + +import javax.annotation.Nonnull; + import org.lowcoder.api.authentication.request.AuthRequestFactory; import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; import org.lowcoder.api.authentication.util.JWTUtils; @@ -14,7 +32,6 @@ import org.lowcoder.infra.constant.NewUrl; import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.util.CookieHelper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -23,6 +40,7 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; @@ -32,48 +50,24 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.server.adapter.ForwardedHeaderTransformer; -import javax.annotation.Nonnull; -import java.util.List; - -import static org.lowcoder.infra.constant.NewUrl.GITHUB_STAR; -import static org.lowcoder.infra.constant.Url.*; -import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER; -import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER_ID; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor @Configuration @EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableReactiveMethodSecurity(useAuthorizationManager = true) public class SecurityConfig { - @Autowired - private CommonConfig commonConfig; - - @Autowired - private SessionUserService sessionUserService; - - @Autowired - private UserService userService; - - @Autowired - private AccessDeniedHandler accessDeniedHandler; - - @Autowired - private ServerAuthenticationEntryPoint serverAuthenticationEntryPoint; - - @Autowired - private CookieHelper cookieHelper; - - @Autowired - AuthenticationService authenticationService; - - @Autowired - AuthenticationApiServiceImpl authenticationApiService; - - @Autowired - AuthRequestFactory authRequestFactory; - - @Autowired - JWTUtils jwtUtils; + private final CommonConfig commonConfig; + private final SessionUserService sessionUserService; + private final UserService userService; + private final AccessDeniedHandler accessDeniedHandler; + private final ServerAuthenticationEntryPoint serverAuthenticationEntryPoint; + private final CookieHelper cookieHelper; + private final AuthenticationService authenticationService; + private final AuthenticationApiServiceImpl authenticationApiService; + private final AuthRequestFactory authRequestFactory; + private final JWTUtils jwtUtils; @Bean SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { @@ -90,7 +84,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http .cors(cors -> cors.configurationSource(buildCorsConfigurationSource())) - .csrf(csrf -> csrf.disable()) + .csrf(CsrfSpec::disable) .anonymous(anonymous -> anonymous.principal(createAnonymousUser())) .httpBasic(Customizer.withDefaults()) .authorizeExchange(customizer -> customizer @@ -146,7 +140,9 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.DATASOURCE_URL + "/jsDatasourcePlugins"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/api/docs/**") ) - .permitAll() + .permitAll() + .pathMatchers("/api/plugins/**") + .permitAll() .pathMatchers("/api/**") .authenticated() .pathMatchers("/test/**") @@ -223,7 +219,7 @@ private CorsConfiguration skipCheckCorsForAllowListDomains() { } @Bean - public ForwardedHeaderTransformer forwardedHeaderTransformer() { + ForwardedHeaderTransformer forwardedHeaderTransformer() { return new ForwardedHeaderTransformer(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java index d5fc1a4eb..fcb066195 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -246,7 +246,7 @@ public Flux getElements(@Nullable String folderId, @Nullable ApplicationType if (folderInfoView == null) { return; } - folderInfoView.setManageable(orgMember.isAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy())); + folderInfoView.setManageable(orgMember.isAdmin() || orgMember.isSuperAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy())); List folderInfoViews = folderNode.getFolderChildren().stream().filter(FolderInfoView::isVisible).toList(); folderInfoView.setSubFolders(folderInfoViews); @@ -340,7 +340,7 @@ private Mono> buildApplicationInfoView private Mono checkManagePermission(String folderId) { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(orgMember); } return isCreator(folderId) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index ae7e2f2c0..4f07b0342 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -1,6 +1,6 @@ package org.lowcoder.api.home; -import static org.lowcoder.infra.event.EventType.APPLICATION_MOVE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_MOVE; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -13,7 +13,11 @@ import org.lowcoder.domain.folder.model.Folder; import org.lowcoder.domain.folder.service.FolderService; import org.lowcoder.domain.permission.model.ResourceRole; -import org.lowcoder.infra.event.EventType; +import org.lowcoder.infra.constant.NewUrl; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java index 9104839d9..a96485eae 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java @@ -18,6 +18,8 @@ public interface SessionUserService { @NonEmptyMono Mono getVisitorOrgMemberCache(); + Mono getVisitorOrgMemberCacheSilent(); + Mono getVisitorOrgMember(); Mono isAnonymousUser(); @@ -33,4 +35,6 @@ public interface SessionUserService { Mono resolveSessionUserForJWT(Claims claims, String token); Mono tokenExist(String token); + + Mono getVisitorToken(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java index 5c0b5e1fe..75b5bec8d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java @@ -1,6 +1,7 @@ package org.lowcoder.api.home; import static org.lowcoder.sdk.constants.GlobalContext.CURRENT_ORG_MEMBER; +import static org.lowcoder.sdk.constants.GlobalContext.VISITOR_TOKEN; import static org.lowcoder.sdk.exception.BizError.UNABLE_TO_FIND_VALID_ORG; import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; import static org.lowcoder.sdk.util.JsonUtils.fromJsonQuietly; @@ -74,6 +75,17 @@ public Mono getVisitorOrgMemberCache() { .switchIfEmpty(deferredError(UNABLE_TO_FIND_VALID_ORG, "UNABLE_TO_FIND_VALID_ORG")); } + @Override + public Mono getVisitorOrgMemberCacheSilent() { + return Mono.deferContextual(contextView -> (Mono) contextView.get(CURRENT_ORG_MEMBER)) + .delayUntil(Mono::just); + } + + @Override + public Mono getVisitorToken() { + return Mono.deferContextual(contextView -> Mono.just(contextView.get(VISITOR_TOKEN))); + } + @Override public Mono getVisitorOrgMember() { return getVisitorId() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java index 968fabc2c..99702c6f2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java @@ -11,7 +11,7 @@ import org.lowcoder.api.util.BusinessEventPublisher; import org.lowcoder.domain.query.model.LibraryQuery; import org.lowcoder.domain.query.service.LibraryQueryService; -import org.lowcoder.infra.event.EventType; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java index c25c78cd4..0bd0300da 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java @@ -58,6 +58,9 @@ public Mono getGroupMembers(String groupId, int page, Mono visitorRoleMono = groupAndOrgMemberInfo.flatMap(tuple -> { GroupMember groupMember = tuple.getT1(); OrgMember orgMember = tuple.getT2(); + if (groupMember.isSuperAdmin() || orgMember.isSuperAdmin()) { + return Mono.just(MemberRole.SUPER_ADMIN); + } if (groupMember.isAdmin() || orgMember.isAdmin()) { return Mono.just(MemberRole.ADMIN); } @@ -109,7 +112,7 @@ private boolean hasReadPermission(Tuple2 tuple) { private boolean hasManagePermission(Tuple2 tuple) { GroupMember groupMember = tuple.getT1(); OrgMember orgMember = tuple.getT2(); - return groupMember.isAdmin() || orgMember.isAdmin(); + return groupMember.isAdmin() || orgMember.isAdmin() || groupMember.isSuperAdmin() || orgMember.isSuperAdmin(); } private Mono> getGroupAndOrgMemberInfo(String groupId) { @@ -175,10 +178,16 @@ public Mono> getGroups() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { String orgId = orgMember.getOrgId(); - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { + MemberRole memberRole; + if(orgMember.isAdmin()) { + memberRole = MemberRole.ADMIN; + } else { + memberRole = MemberRole.SUPER_ADMIN; + } return groupService.getByOrgId(orgId) .sort() - .flatMapSequential(group -> GroupView.from(group, MemberRole.ADMIN.getValue())) + .flatMapSequential(group -> GroupView.from(group, memberRole.getValue())) .collectList(); } return groupMemberService.getUserGroupMembersInOrg(orgId, orgMember.getUserId()) @@ -211,7 +220,7 @@ public Mono deleteGroup(String groupId) { public Mono create(CreateGroupRequest createGroupRequest) { return sessionUserService.getVisitorOrgMemberCache() - .filter(OrgMember::isAdmin) + .filter(orgMember -> orgMember.isAdmin() || orgMember.isSuperAdmin()) .switchIfEmpty(deferredError(BizError.NOT_AUTHORIZED, NOT_AUTHORIZED)) .delayUntil(orgMember -> bizThresholdChecker.checkMaxGroupCount(orgMember)) .flatMap(orgMember -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 6663e09cb..ac3023f74 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -270,7 +270,7 @@ public Mono create(Organization organization) { return sessionUserService.getVisitorId() .delayUntil(userId -> bizThresholdChecker.checkMaxOrgCount(userId)) .delayUntil(__ -> checkIfSaasMode()) - .flatMap(userId -> organizationService.create(organization, userId)) + .flatMap(userId -> organizationService.create(organization, userId, false)) .map(OrgView::new); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java index fc247766a..315c5f6f2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java @@ -44,7 +44,7 @@ public Mono checkCurrentOrgDev() { public Mono isCurrentOrgDev() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(true); } return inDevGroup(orgMember.getOrgId(), orgMember.getUserId()); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 42161bd5a..252a4f837 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -46,7 +46,7 @@ public Mono getUserDetailById(String userId) { private Mono checkAdminPermissionAndUserBelongsToCurrentOrg(String userId) { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (!orgMember.isAdmin()) { + if (!orgMember.isAdmin() && !orgMember.isSuperAdmin()) { return ofError(UNSUPPORTED_OPERATION, "BAD_REQUEST"); } return orgMemberService.getOrgMember(orgMember.getOrgId(), userId) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java new file mode 100644 index 000000000..109d5abd5 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java @@ -0,0 +1,90 @@ +package org.lowcoder.api.util; + +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.lowcoder.api.framework.filter.ReactiveRequestContextHolder; +import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.domain.organization.model.OrgMember; +import org.lowcoder.infra.event.APICallEvent; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.lowcoder.sdk.constants.Authentication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +import static org.springframework.http.HttpHeaders.writableHttpHeaders; + +@Slf4j +@Aspect +@Component +public class ApiCallEventPublisher { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private SessionUserService sessionUserService; + + @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") + public void getMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") + public void postMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)") + public void putMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)") + public void deleteMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PatchMapping)") + public void patchMapping(){} + + @Around("(getMapping() || postMapping() || putMapping() || deleteMapping() || patchMapping())") + public Object handleAPICallEvent(ProceedingJoinPoint joinPoint) throws Throwable { + + return sessionUserService.getVisitorToken() + .zipWith(sessionUserService.getVisitorOrgMemberCacheSilent().defaultIfEmpty(OrgMember.NOT_EXIST)) + .zipWith(ReactiveRequestContextHolder.getRequest()) + .doOnNext( + tuple -> { + String token = tuple.getT1().getT1(); + OrgMember orgMember = tuple.getT1().getT2(); + ServerHttpRequest request = tuple.getT2(); + if (orgMember == OrgMember.NOT_EXIST) { + return; + } + MultiValueMap headers = writableHttpHeaders(request.getHeaders()); + headers.remove("Cookie"); + String ipAddress = headers.remove("X-Real-IP").stream().findFirst().get(); + APICallEvent event = APICallEvent.builder() + .userId(orgMember.getUserId()) + .orgId(orgMember.getOrgId()) + .type(EventType.API_CALL_EVENT) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) + .httpMethod(request.getMethod().name()) + .requestUri(request.getURI().getPath()) + .headers(headers) + .queryParams(request.getQueryParams()) + .ipAddress(ipAddress) + .build(); + event.populateDetails(); + applicationEventPublisher.publishEvent(event); + }) + .onErrorResume(throwable -> { + log.error("handleAPICallEvent error {} for: {} ", joinPoint.getSignature().getName(), EventType.API_CALL_EVENT, throwable); + return Mono.empty(); + }) + .then((Mono) joinPoint.proceed()); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java index e81f5136c..850c33d78 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java @@ -1,15 +1,7 @@ package org.lowcoder.api.util; -import static org.lowcoder.domain.permission.model.ResourceHolder.USER; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -import javax.annotation.Nullable; - +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.application.view.ApplicationInfoView; import org.lowcoder.api.application.view.ApplicationView; @@ -32,7 +24,6 @@ import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.service.UserService; import org.lowcoder.infra.event.ApplicationCommonEvent; -import org.lowcoder.infra.event.EventType; import org.lowcoder.infra.event.FolderCommonEvent; import org.lowcoder.infra.event.LibraryQueryEvent; import org.lowcoder.infra.event.QueryExecutionEvent; @@ -47,14 +38,20 @@ import org.lowcoder.infra.event.groupmember.GroupMemberRoleUpdateEvent; import org.lowcoder.infra.event.user.UserLoginEvent; import org.lowcoder.infra.event.user.UserLogoutEvent; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.lowcoder.sdk.constants.Authentication; import org.lowcoder.sdk.util.LocaleUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; - -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.lowcoder.domain.permission.model.ResourceHolder.USER; + @Slf4j @Component public class BusinessEventPublisher { @@ -77,16 +74,24 @@ public class BusinessEventPublisher { private ResourcePermissionService resourcePermissionService; public Mono publishFolderCommonEvent(String folderId, String folderName, EventType eventType) { - return sessionUserService.getVisitorOrgMemberCache() - .doOnNext(orgMember -> { - FolderCommonEvent event = FolderCommonEvent.builder() - .id(folderId) - .name(folderName) - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) - .type(eventType) - .build(); - applicationEventPublisher.publishEvent(event); + + return sessionUserService.getVisitorToken() + .zipWith(sessionUserService.getVisitorOrgMemberCache()) + .doOnNext( + tuple -> { + String token = tuple.getT1(); + OrgMember orgMember = tuple.getT2(); + FolderCommonEvent event = FolderCommonEvent.builder() + .id(folderId) + .name(folderName) + .userId(orgMember.getUserId()) + .orgId(orgMember.getOrgId()) + .type(eventType) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) + .build(); + event.populateDetails(); + applicationEventPublisher.publishEvent(event); }) .then() .onErrorResume(throwable -> { @@ -106,6 +111,7 @@ public Mono publishApplicationCommonEvent(String applicationId, @Nullable return ApplicationView.builder() .applicationInfoView(applicationInfoView) .build(); + }) .flatMap(applicationView -> publishApplicationCommonEvent(applicationView, eventType)); } @@ -126,9 +132,11 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, .map(Optional::of) .onErrorReturn(Optional.empty()); })) + .zipWith(sessionUserService.getVisitorToken()) .doOnNext(tuple -> { - OrgMember orgMember = tuple.getT1(); - Optional optional = tuple.getT2(); + OrgMember orgMember = tuple.getT1().getT1(); + Optional optional = tuple.getT1().getT2(); + String token = tuple.getT2(); ApplicationInfoView applicationInfoView = applicationView.getApplicationInfoView(); ApplicationCommonEvent event = ApplicationCommonEvent.builder() .orgId(orgMember.getOrgId()) @@ -138,7 +146,10 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, .type(eventType) .folderId(optional.map(Folder::getId).orElse(null)) .folderName(optional.map(Folder::getName).orElse(null)) + .isAnonymous(anonymous) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -150,13 +161,18 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, } public Mono publishUserLoginEvent(String source) { - return sessionUserService.getVisitorOrgMember() - .doOnNext(orgMember -> { + return sessionUserService.getVisitorOrgMember().zipWith(sessionUserService.getVisitorToken()) + .doOnNext(tuple -> { + OrgMember orgMember = tuple.getT1(); + String token = tuple.getT2(); UserLoginEvent event = UserLoginEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) .source(source) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -168,11 +184,17 @@ public Mono publishUserLoginEvent(String source) { public Mono publishUserLogoutEvent() { return sessionUserService.getVisitorOrgMemberCache() - .doOnNext(orgMember -> { + .zipWith(sessionUserService.getVisitorToken()) + .doOnNext(tuple -> { + OrgMember orgMember = tuple.getT1(); + String token = tuple.getT2(); UserLogoutEvent event = UserLogoutEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -184,15 +206,19 @@ public Mono publishUserLogoutEvent() { public Mono publishGroupCreateEvent(Group group) { return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupCreateEvent event = GroupCreateEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(group.getId()) .groupName(group.getName(locale)) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -208,15 +234,19 @@ public Mono publishGroupUpdateEvent(boolean publish, Group previousGroup, return Mono.empty(); } return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupUpdateEvent event = GroupUpdateEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(previousGroup.getId()) .groupName(previousGroup.getName(locale) + " => " + newGroupName) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -232,15 +262,19 @@ public Mono publishGroupDeleteEvent(boolean publish, Group previousGroup) return Mono.empty(); } return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupDeleteEvent event = GroupDeleteEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(previousGroup.getId()) .groupName(previousGroup.getName(locale)) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -257,13 +291,15 @@ public Mono publishGroupMemberAddEvent(boolean publish, String groupId, Ad } return Mono.zip(groupService.getById(groupId), sessionUserService.getVisitorOrgMemberCache(), - userService.findById(addMemberRequest.getUserId())) + userService.findById(addMemberRequest.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); Group group = tuple.getT1(); OrgMember orgMember = tuple.getT2(); User member = tuple.getT3(); + String token = tuple.getT4(); GroupMemberAddEvent event = GroupMemberAddEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) @@ -272,7 +308,10 @@ public Mono publishGroupMemberAddEvent(boolean publish, String groupId, Ad .memberId(member.getId()) .memberName(member.getName()) .memberRole(addMemberRequest.getRole()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -290,7 +329,8 @@ public Mono publishGroupMemberRoleUpdateEvent(boolean publish, String grou } return Mono.zip(groupService.getById(groupId), sessionUserService.getVisitorOrgMemberCache(), - userService.findById(previousGroupMember.getUserId())) + userService.findById(previousGroupMember.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -305,7 +345,10 @@ public Mono publishGroupMemberRoleUpdateEvent(boolean publish, String grou .memberId(member.getId()) .memberName(member.getName()) .memberRole(previousGroupMember.getRole().getValue() + " => " + updateRoleRequest.getRole()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -322,7 +365,8 @@ public Mono publishGroupMemberLeaveEvent(boolean publish, GroupMember grou } return Mono.zip(groupService.getById(groupMember.getGroupId()), userService.findById(groupMember.getUserId()), - sessionUserService.getVisitorOrgMemberCache()) + sessionUserService.getVisitorOrgMemberCache(), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -337,7 +381,10 @@ public Mono publishGroupMemberLeaveEvent(boolean publish, GroupMember grou .memberId(user.getId()) .memberName(user.getName()) .memberRole(groupMember.getRole().getValue()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -354,7 +401,8 @@ public Mono publishGroupMemberRemoveEvent(boolean publish, GroupMember pre } return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), groupService.getById(previousGroupMember.getGroupId()), - userService.findById(previousGroupMember.getUserId())) + userService.findById(previousGroupMember.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -369,7 +417,10 @@ public Mono publishGroupMemberRemoveEvent(boolean publish, GroupMember pre .memberId(member.getId()) .memberName(member.getName()) .memberRole(previousGroupMember.getRole().getValue()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -395,15 +446,19 @@ public Mono publishDatasourceEvent(String id, EventType eventType) { public Mono publishDatasourceEvent(Datasource datasource, EventType eventType) { return sessionUserService.getVisitorOrgMemberCache() - .flatMap(orgMember -> { + .zipWith(sessionUserService.getVisitorToken()) + .flatMap(tuple -> { DatasourceEvent event = DatasourceEvent.builder() .datasourceId(datasource.getId()) .name(datasource.getName()) .type(datasource.getType()) .eventType(eventType) - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) + .userId(tuple.getT1().getUserId()) + .orgId(tuple.getT1().getOrgId()) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono. empty(); }) @@ -435,7 +490,9 @@ public Mono publishDatasourcePermissionEvent(String permissionId, EventTyp public Mono publishDatasourcePermissionEvent(String datasourceId, Collection userIds, Collection groupIds, String role, EventType eventType) { - return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), datasourceService.getById(datasourceId)) + return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), + datasourceService.getById(datasourceId), + sessionUserService.getVisitorToken()) .flatMap(tuple -> { OrgMember orgMember = tuple.getT1(); Datasource datasource = tuple.getT2(); @@ -449,7 +506,10 @@ public Mono publishDatasourcePermissionEvent(String datasourceId, .groupIds(groupIds) .role(role) .eventType(eventType) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT3(), StandardCharsets.UTF_8).toString()) .build(); + datasourcePermissionEvent.populateDetails(); applicationEventPublisher.publishEvent(datasourcePermissionEvent); return Mono. empty(); }) @@ -465,13 +525,20 @@ public Mono publishLibraryQuery(LibraryQuery libraryQuery, EventType event public Mono publishLibraryQueryEvent(String id, String name, EventType eventType) { return sessionUserService.getVisitorOrgMemberCache() - .map(orgMember -> LibraryQueryEvent.builder() - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) - .id(id) - .name(name) - .eventType(eventType) - .build()) + .zipWith(sessionUserService.getVisitorToken()) + .map(tuple -> { + LibraryQueryEvent event = LibraryQueryEvent.builder() + .userId(tuple.getT1().getUserId()) + .orgId(tuple.getT1().getOrgId()) + .id(id) + .name(name) + .eventType(eventType) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) + .build(); + event.populateDetails(); + return event; + }) .doOnNext(applicationEventPublisher::publishEvent) .then() .onErrorResume(throwable -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java new file mode 100644 index 000000000..57701daa8 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java @@ -0,0 +1,28 @@ +package org.lowcoder.api.util; + +import org.passay.CharacterData; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.PasswordGenerator; + +public class RandomPasswordGeneratorConfig { + + public String generatePassayPassword() { + PasswordGenerator gen = new PasswordGenerator(); + CharacterData lowerCaseChars = EnglishCharacterData.LowerCase; + CharacterRule lowerCaseRule = new CharacterRule(lowerCaseChars); + lowerCaseRule.setNumberOfCharacters(3); + + CharacterData upperCaseChars = EnglishCharacterData.UpperCase; + CharacterRule upperCaseRule = new CharacterRule(upperCaseChars); + upperCaseRule.setNumberOfCharacters(3); + + CharacterData digitChars = EnglishCharacterData.Digit; + CharacterRule digitRule = new CharacterRule(digitChars); + digitRule.setNumberOfCharacters(3); + + + String password = gen.generatePassword(10, lowerCaseRule, upperCaseRule, digitRule); + return password; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index 6e33d075b..5364a5931 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -18,6 +18,7 @@ import org.lowcoder.infra.config.model.ServerConfig; import org.lowcoder.infra.eventlog.EventLog; import org.lowcoder.infra.serverlog.ServerLog; +import org.lowcoder.runner.migrations.job.AddSuperAdminUser; import org.lowcoder.runner.migrations.job.AddPtmFieldsJob; import org.lowcoder.runner.migrations.job.CompleteAuthType; import org.lowcoder.runner.migrations.job.MigrateAuthConfigJob; @@ -183,7 +184,12 @@ public void addOrgIdIndexOnServerLog(MongockTemplate mongoTemplate) { ); } - @ChangeSet(order = "020", id = "add-ptm-fields-to-applications", author = "") + @ChangeSet(order = "020", id = "add-super-admin-user", author = "") + public void addSuperAdminUser(AddSuperAdminUser addSuperAdminUser) { + addSuperAdminUser.addSuperAdmin(); + } + + @ChangeSet(order = "021", id = "add-ptm-fields-to-applications", author = "") public void addPtmFieldsToApplicatgions(AddPtmFieldsJob addPtmFieldsJob) { addPtmFieldsJob.migrateApplicationsToInitPtmFields(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java new file mode 100644 index 000000000..2aea53af3 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java @@ -0,0 +1,6 @@ +package org.lowcoder.runner.migrations.job; + +public interface AddSuperAdminUser { + + void addSuperAdmin(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java new file mode 100644 index 000000000..72e7391d7 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java @@ -0,0 +1,67 @@ +package org.lowcoder.runner.migrations.job; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; +import org.lowcoder.api.util.RandomPasswordGeneratorConfig; +import org.lowcoder.domain.authentication.context.AuthRequestContext; +import org.lowcoder.domain.authentication.context.FormAuthRequestContext; +import org.lowcoder.domain.user.model.AuthUser; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG; + +@RequiredArgsConstructor +@Component +@Slf4j(topic = "AddSuperAdminUserImpl") +public class AddSuperAdminUserImpl implements AddSuperAdminUser { + + private final AuthenticationApiServiceImpl authenticationApiService; + private final CommonConfig commonConfig; + + @Override + public void addSuperAdmin() { + + AuthUser authUser = formulateAuthUser(); + + authenticationApiService.updateOrCreateUser(authUser, false) + .delayUntil(user -> { + if (user.getIsNewUser()) { + return authenticationApiService.onUserRegister(user, true); + } + return Mono.empty(); + }) + .block(); + } + + private AuthUser formulateAuthUser() { + String username = formulateUserName(); + String password = formulatePassword(); + AuthRequestContext authRequestContext = new FormAuthRequestContext(username, password, true, null); + authRequestContext.setAuthConfig(DEFAULT_AUTH_CONFIG); + return AuthUser.builder() + .uid(username) + .username(username) + .authContext(authRequestContext) + .build(); + } + private String formulateUserName() { + if(commonConfig.getSuperAdmin().getUserName() != null) { + return commonConfig.getSuperAdmin().getUserName(); + } + return "admin@lowcoder.pro"; + } + + private String formulatePassword() { + if(commonConfig.getSuperAdmin().getPassword() != null) { + return commonConfig.getSuperAdmin().getPassword(); + } + RandomPasswordGeneratorConfig passGen = new RandomPasswordGeneratorConfig(); + String password = passGen.generatePassayPassword(); + log.info("PASSWORD FOR SUPER-ADMIN is: {}", password); + return password; + } +} diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 66d022e68..d7ad21a53 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -10,7 +10,14 @@ spring: allow-bean-definition-overriding: true allow-circular-references: true +logging: + level: + root: info + web: debug + server: + error: + includeStacktrace: ALWAYS compression: enabled: true forward-headers-strategy: NATIVE @@ -44,6 +51,11 @@ common: block-hound-enable: false js-executor: host: http://127.0.0.1:6060 + plugin-dirs: + - /tmp/plugins + super-admin: + username: test@lowcoder.pro + password: Password@123 marketplace: private-mode: false diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 258833aea..30cd78b3b 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -17,7 +17,7 @@ spring: codec: max-in-memory-size: 20MB webflux: - context-path: / + base-path: / server: compression: @@ -53,6 +53,8 @@ common: max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120} workspace: mode: ${LOWCODER_WORKSPACE_MODE:SAAS} + plugin-dirs: + - ${LOWCODER_PLUGINS_DIR:plugins} marketplace: private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true} diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 23ffce7ad..a04ba2dd2 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -1,335 +1,156 @@ - - - - org.springframework.boot - spring-boot-starter-parent - 3.1.1 - - - - 4.0.0 - org.lowcoder - lowcoder-root - ${revision} - pom - lowcoder-root - - - 2.3.0-SNAPSHOT - 17 - true - true - true - org.lowcoder - 1.0-SNAPSHOT - true - 2.17.0 - 17 - 17 - - - - - sonatype - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - cloud - - cloud - - - true - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/selfhost/application*.yml - - - - - - - selfhost - - selfhost - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/application*.yml - - - - - - - - - - - org.codehaus.mojo - license-maven-plugin - 2.0.0 - - - maven-dependency-plugin - 3.1.2 - - - - - - - + + + 4.0.0 + org.lowcoder + lowcoder-root + pom + lowcoder-root + ${revision} + + + + 2.3.0-SNAPSHOT + 17 + true + true + true + org.lowcoder + 1.0-SNAPSHOT + true + 2.17.0 + 17 + 17 + + + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + + + + cloud + + cloud + + + true + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/selfhost/application*.yml + + + + + + + selfhost + + selfhost + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/application*.yml + + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + maven-dependency-plugin + + + + + + maven-assembly-plugin + 3.6.0 + + + src/assembly/bin.xml + + + + + + + + + + org.lowcoder lowcoder-sdk ${revision} - +
org.lowcoder lowcoder-infra ${revision} - + org.lowcoder lowcoder-domain ${revision} - + org.lowcoder lowcoder-plugins ${revision} - + org.lowcoder lowcoder-server ${revision} - - - - org.pf4j - pf4j - 3.5.0 - - - - org.json - json - 20230227 - - - - org.projectlombok - lombok - 1.18.26 - - - - org.apache.commons - commons-text - 1.10.0 - - - commons-io - commons-io - 2.13.0 - - - org.glassfish - javax.el - 3.0.0 - - - javax.el - javax.el-api - 3.0.0 - - - - org.eclipse.jgit - org.eclipse.jgit - 6.7.0.202309050840-r - - - - org.apache.commons - commons-collections4 - 4.4 - - - com.google.guava - guava - 30.0-jre - - - - tv.twelvetone.rjson - rjson - 1.3.1-SNAPSHOT - - - org.jetbrains.kotlin - kotlin-stdlib-jdk7 - 1.6.21 - - - - com.jayway.jsonpath - json-path - 2.7.0 - - - com.github.ben-manes.caffeine - caffeine - 3.0.5 - - - es.moki.ratelimitj - ratelimitj-core - 0.7.0 - - - com.github.spullara.mustache.java - compiler - 0.9.6 - - - - es.moki.ratelimitj - ratelimitj-redis - 0.7.0 - - - - io.projectreactor - reactor-core - 3.4.29 - - - - org.pf4j - pf4j-spring - 0.8.0 - - - - com.querydsl - querydsl-apt - 5.0.0 - - - - io.sentry - sentry-spring-boot-starter - 3.1.2 - - - - org.jgrapht - jgrapht-core - 1.5.0 - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - javax.activation - activation - 1.1.1 - - - - org.glassfish.jaxb - jaxb-runtime - 2.3.3 - - - - com.github.cloudyrock.mongock - mongock-bom - 4.3.8 - pom - import - - - - io.projectreactor.tools - blockhound - 1.0.6.RELEASE - - - - jakarta.servlet - jakarta.servlet-api - 6.0.0 - - - io.projectreactor - reactor-test - 3.3.5.RELEASE - - - org.apache.httpcomponents - httpclient - 4.5.14 - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - 4.7.0 - - - org.mockito - mockito-inline - 5.2.0 - test - - - javax.validation - validation-api - 2.0.1.Final - - - - - - lowcoder-sdk - lowcoder-infra - lowcoder-domain - lowcoder-plugins - lowcoder-server - + + + + + lowcoder-dependencies + lowcoder-sdk + lowcoder-infra + lowcoder-domain + lowcoder-plugins + lowcoder-server + distribution + From da0c2aa4d07d92ae9a9b1aee1a141d14cf1dd7d3 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Tue, 5 Mar 2024 23:44:16 +0100 Subject: [PATCH 21/33] new: plugin endpoint security basics --- server/api-service/PLUGIN.md | 65 +---------------- server/api-service/lowcoder-sdk/pom.xml | 19 ++--- .../application/ApplicationController.java | 5 +- .../framework/plugin/PluginClassLoader.java | 12 ++-- .../endpoint/PluginEndpointHandlerImpl.java | 71 ++++++++++--------- .../EndpointAuthorizationManager.java | 24 +++++++ .../security/PluginAuthorizationManager.java | 8 +-- .../plugin/security/SecuredEndpoint.java | 16 +++++ server/api-service/pom.xml | 7 +- 9 files changed, 105 insertions(+), 122 deletions(-) create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java diff --git a/server/api-service/PLUGIN.md b/server/api-service/PLUGIN.md index 92fb50ad9..65a99adef 100644 --- a/server/api-service/PLUGIN.md +++ b/server/api-service/PLUGIN.md @@ -1,4 +1,4 @@ -# Lowcoder plugin system (WIP) +# Lowcoder backend plugin system This is an ongoing effort to refactor current plugin system based on pf4j library. @@ -50,73 +50,14 @@ Plugin jar can be structured in any way you like. It can be a plain java project It is composed from several parts: - class(es) implementing **LowcoderPlugin** interface -- class(es) implementing **LowcoderEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: +- class(es) implementing **PluginEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: ```java @EndpointExtension(uri = , method = ) - public Mono (ServerRequest request) + public EndpointResponse (EndpointRequest request) { ... your endpoint logic implementation } - - for example: - - @EndpointExtension(uri = "/hello-world", method = Method.GET) - public Mono helloWorld(ServerRequest request) - { - return ServerResponse.ok().body(Mono.just(Hello.builder().message("Hello world!").build()), Hello.class); - } ``` - TODO: class(es) impelemting **LowcoderDatasource** interface -### LowcoderPlugin implementations - -Methods of interest: -- **pluginId()** - unique plugin ID - if a plugin with such ID is already loaded, subsequent plugins whith this ID will be ignored -- **description()** - short plugin description -- **load(ApplicationContext parentContext)** - is called during plugin startup - this is the place where you should completely initialize your plugin. If initialization fails, return false -- **unload()** - is called during lowcoder API server shutdown - this is the place where you should release all resources -- **endpoints()** - needs to contain all initialized **PluginEndpoints** you want to expose, for example: - -```java - @Override - public List endpoints() - { - List endpoints = new ArrayList<>(); - - endpoints.add(new HelloWorldEndpoint()); - - return endpoints; - } -``` -- **pluginInfo()** - should return a record object with additional information about your plugin. It is serialized to JSON as part of the **/plugins** listing (see **"info"** object in this example): - -```json -[ - { - "id": "example-plugin", - "description": "Example plugin for lowcoder platform", - "info": {} - }, - { - "id": "enterprise", - "description": "Lowcoder enterprise plugin", - "info": { - "enabledFeatures": [ - "endpointApiUsage" - ] - } - } -] -``` - -## TODOs - -1. Implement endpoint security - currently all plugin endpoints are public (probably by adding **security** attribute to **@EndpointExtension** and enforcing it) - - -## QUESTIONS / CONSIDERATIONS - -1. currently the plugin endpoints are prefixed with **/plugin/{pluginId}/** - this is hardcoded, do we want to make it configurable? - - diff --git a/server/api-service/lowcoder-sdk/pom.xml b/server/api-service/lowcoder-sdk/pom.xml index 9918359bc..22e6cb815 100644 --- a/server/api-service/lowcoder-sdk/pom.xml +++ b/server/api-service/lowcoder-sdk/pom.xml @@ -13,13 +13,6 @@ lowcoder-sdk - - UTF-8 - UTF-8 - - 17 - - org.springframework.boot @@ -173,7 +166,17 @@ validation-api - + + + UTF-8 + UTF-8 + + 17 + + 17 + 17 + + diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index 61f9a79ca..de398e01f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -27,7 +27,6 @@ import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; import org.lowcoder.domain.permission.model.ResourceRole; -import org.lowcoder.infra.event.EventType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -106,7 +105,7 @@ public Mono> getPublishedApplication(@PathVariable public Mono> getPublishedMarketPlaceApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_MARKETPLACE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } @@ -114,7 +113,7 @@ public Mono> getPublishedMarketPlaceApplication(@P public Mono> getAgencyProfileApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.AGENCY_PROFILE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java index e0be8dac2..34945cdaf 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java @@ -1,7 +1,6 @@ package org.lowcoder.api.framework.plugin; import java.io.IOException; -import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; @@ -20,6 +19,11 @@ public class PluginClassLoader extends URLClassLoader private static final ClassLoader baseClassLoader = ClassLoader.getPlatformClassLoader(); private final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + private static final String[] excludedPaths = new String[] { + "org.lowcoder.plugin.api.", + "org/lowcoder/plugin/api/" + }; + public PluginClassLoader(String name, Path pluginPath) { super(name, pathToURLs(pluginPath), baseClassLoader); @@ -34,7 +38,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE return clazz; } - if (name.startsWith("org.lowcoder.plugin.api.")) + if (StringUtils.startsWithAny(name, excludedPaths)) { try { @@ -67,7 +71,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE @Override public URL getResource(String name) { Objects.requireNonNull(name); - if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + if (StringUtils.startsWithAny(name, excludedPaths)) { return appClassLoader.getResource(name); } @@ -79,7 +83,7 @@ public URL getResource(String name) { public Enumeration getResources(String name) throws IOException { Objects.requireNonNull(name); - if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + if (StringUtils.startsWithAny(name, excludedPaths)) { return appClassLoader.getResources(name); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java index bcee69580..214252827 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -16,19 +16,23 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.framework.plugin.data.PluginServerRequest; +import org.lowcoder.api.framework.plugin.security.SecuredEndpoint; import org.lowcoder.plugin.api.EndpointExtension; import org.lowcoder.plugin.api.PluginEndpoint; import org.lowcoder.plugin.api.data.EndpointRequest; import org.lowcoder.plugin.api.data.EndpointResponse; import org.lowcoder.sdk.exception.BaseException; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.aop.target.SimpleBeanTargetSource; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.ResolvableType; import org.springframework.http.ResponseCookie; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RequestPredicate; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; @@ -80,48 +84,47 @@ public List> registeredEndpoints() private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler) { - if (handler.isAnnotationPresent(EndpointExtension.class)) + if (!handler.isAnnotationPresent(EndpointExtension.class) || !checkHandlerMethod(handler)) { - if (checkHandlerMethod(handler)) + if (handler.isAnnotationPresent(EndpointExtension.class)) { - - EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); - String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); - - RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> - { - Mono result = null; - try - { - EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(req)); - result = createServerResponse(response); - } - catch (IllegalAccessException | InvocationTargetException cause) - { - throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); - } - return result; - }); - routes.add(routerFunction); - registerRouterFunctionMapping(endpointName, routerFunction); - - log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); - } - else - { - log.error("Cannot register plugin endpoint: {} -> {}! Handler method must be defined as: public Mono {}(ServerRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); + log.debug("Not registering plugin endpoint method: {} -> {}! Handler method must be defined as: public EndpointResponse methodName(EndpointRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); } + return; } + + EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); + String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); + RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> runPluginEndpointMethod(endpoint, endpointMeta, handler, req)); + routes.add(routerFunction); + registerRouterFunctionMapping(endpointName, routerFunction); + + log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); } + @SecuredEndpoint + public Mono runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request) + { + Mono result = null; + try + { + log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request); + + EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request)); + result = createServerResponse(response); + } + catch (IllegalAccessException | InvocationTargetException cause) + { + throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); + } + return result; + } + + private void registerRouterFunctionMapping(String endpointName, RouterFunction routerFunction) { String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); - - ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> { - return routerFunction; - }); - + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction ); log.debug("Registering RouterFunction bean definition: {}", beanName); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java new file mode 100644 index 000000000..6ad509044 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java @@ -0,0 +1,24 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class EndpointAuthorizationManager implements AuthorizationManager +{ + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) + { + log.info("Checking plugin endpoint invocation security for {}", invocation.getMethod().getName()); + + return new AuthorizationDecision(true); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java index e1849c444..237567643 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -5,7 +5,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.lang3.StringUtils; import org.lowcoder.plugin.api.EndpointExtension; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; @@ -21,7 +20,7 @@ import reactor.core.publisher.Mono; @Slf4j -@Component +//@Component public class PluginAuthorizationManager implements ReactiveAuthorizationManager { private final MethodSecurityExpressionHandler expressionHandler; @@ -34,10 +33,9 @@ public PluginAuthorizationManager() @Override public Mono check(Mono authentication, MethodInvocation invocation) { - log.info(" invocation :: {}", invocation.getMethod()); + log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName()); - Method method = invocation.getMethod(); - EndpointExtension endpointExtension = AnnotationUtils.findAnnotation(method, EndpointExtension.class); + EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1]; if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize())) { return Mono.empty(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java new file mode 100644 index 000000000..aadc0c7fd --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java @@ -0,0 +1,16 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface SecuredEndpoint { + +} diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index a04ba2dd2..8ec6f774d 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -12,17 +12,12 @@ - 2.3.0-SNAPSHOT + 2.4.0 17 true true true - org.lowcoder - 1.0-SNAPSHOT true - 2.17.0 - 17 - 17 From 9f0e484f6d5a557ef3d459e73f0c972e1ee109d4 Mon Sep 17 00:00:00 2001 From: liusijun Date: Mon, 11 Mar 2024 16:58:35 +0800 Subject: [PATCH 22/33] fix: complete the locale file --- client/packages/lowcoder/src/i18n/locales/zh.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index d2e868a61..78f0fc071 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -140,7 +140,7 @@ prop: { expand: "展开", columns: "列", rowSelection: "行选择", - toolbar: "工具栏", + toolbar: "工具栏", pagination: "分页", logo: "标志", style: "样式", @@ -358,6 +358,7 @@ style: { textSize: "字体大小", textWeight: "字体粗细", "fontFamily": "字体", + "fontStyle": "字体风格", "backgroundImage": "背景图片", "backgroundImageRepeat" : "背景图片重复", "backgroundImageSize" : "背景图片大小", @@ -1086,7 +1087,7 @@ selectInput: { valueDesc: "当前选择的值", selectedIndexDesc: "当前选择的值的索引,如果未选择任何值则为-1", selectedLabelDesc: "当前选择的值的标签", -}, +}, file: { typeErrorMsg: "必须是一个带有有效文件大小单位的数字,或者是一个无单位的字节数.", fileEmptyErrorMsg: "上传失败.文件大小为空.", @@ -2327,7 +2328,7 @@ componentDoc: { event: "事件", eventName: "事件名称", eventDesc: "描述", - mehtod: "方法", + mehtod: "方法", methodUsage: "您可以通过方法与组件进行交互,并且可以在任何可以编写 JavaScript 的地方通过它们的名称调用它们.或者您可以通过事件的“控制组件”操作来调用它们.", methodName: "方法名称", methodDesc: "描述", @@ -2561,7 +2562,7 @@ componentDocExtra: { table: table, }, idSource: { - title: "用户认证提供商", + title: "用户认证提供商", form: "电子邮件", pay: "高级", enable: "启用", @@ -2723,4 +2724,4 @@ timeLine: { navStyle: "菜单风格", navItemStyle: "菜单项样式", } -}; \ No newline at end of file +}; From c14b91abfa038fd25edbcc2a0adeaf7064ab8faa Mon Sep 17 00:00:00 2001 From: liusijun Date: Tue, 12 Mar 2024 17:26:32 +0800 Subject: [PATCH 23/33] fix: coding standard and typo --- .../lowcoder-design/src/icons/index.ts | 2 +- .../numberInputComp/sliderCompConstants.tsx | 4 +- .../tableComp/column/tableColumnComp.tsx | 6 +- .../comps/comps/timelineComp/timelineComp.tsx | 4 +- .../comps/timelineComp/timelineConstants.tsx | 8 +- .../comps/timelineComp/timelineUtils.tsx | 2 +- .../comps/triContainerComp/triContainer.tsx | 14 ++-- .../src/comps/controls/styleControl.tsx | 26 +++---- .../comps/controls/styleControlConstants.tsx | 71 ++++++++--------- .../packages/lowcoder/src/i18n/locales/de.ts | 14 ++-- .../packages/lowcoder/src/i18n/locales/en.ts | 76 +++++++++---------- .../i18n/locales/translation_files/de.json | 14 ++-- .../i18n/locales/translation_files/en.json | 72 +++++++++--------- .../i18n/locales/translation_files/es.json | 2 +- .../i18n/locales/translation_files/fr.json | 2 +- .../packages/lowcoder/src/i18n/locales/zh.ts | 12 +-- .../pages/setting/theme/detail/previewDsl.ts | 2 +- 17 files changed, 166 insertions(+), 165 deletions(-) diff --git a/client/packages/lowcoder-design/src/icons/index.ts b/client/packages/lowcoder-design/src/icons/index.ts index 99c7b553c..71c1df6ab 100644 --- a/client/packages/lowcoder-design/src/icons/index.ts +++ b/client/packages/lowcoder-design/src/icons/index.ts @@ -290,7 +290,7 @@ export { ReactComponent as WidthIcon } from "./icon-width.svg"; export { ReactComponent as ResponsiveLayoutCompIcon } from "./icon-responsive-layout-comp.svg"; export { ReactComponent as TextSizeIcon } from "./remix/font-size-2.svg"; export { ReactComponent as FontFamilyIcon } from "./remix/font-sans-serif.svg"; -export { ReactComponent as TextWeigthIcon } from "./remix/bold.svg"; +export { ReactComponent as TextWeightIcon } from "./remix/bold.svg"; export { ReactComponent as BorderWidthIcon } from "./remix/expand-width-line.svg"; export { ReactComponent as LeftInfoLine } from "./remix/information-line.svg"; export { ReactComponent as LeftInfoFill } from "./remix/information-fill.svg"; diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx index 601a72c0d..c46103457 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx @@ -30,7 +30,7 @@ const getStyle = (style: SliderStyleType) => { } .ant-slider-handle { background-color: ${style.thumb}; - border-color: ${style.thumbBoder}; + border-color: ${style.thumbBorder}; } } &:hover { @@ -39,7 +39,7 @@ const getStyle = (style: SliderStyleType) => { } } .ant-slider-handle:focus { - box-shadow: 0 0 0 5px ${fadeColor(darkenColor(style.thumbBoder, 0.08), 0.12)}; + box-shadow: 0 0 0 5px ${fadeColor(darkenColor(style.thumbBorder, 0.08), 0.12)}; } } `; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx index 6a80483c0..40c135661 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx @@ -21,7 +21,7 @@ import { withFunction, wrapChildAction, } from "lowcoder-core"; -import { AlignClose, AlignLeft, AlignRight, IconRadius, BorderWidthIcon, TextSizeIcon, FontFamilyIcon, TextWeigthIcon, ImageCompIcon, controlItem, Dropdown, OptionType } from "lowcoder-design"; +import { AlignClose, AlignLeft, AlignRight, IconRadius, BorderWidthIcon, TextSizeIcon, FontFamilyIcon, TextWeightIcon, ImageCompIcon, controlItem, Dropdown, OptionType } from "lowcoder-design"; import { ColumnTypeComp, ColumnTypeCompMap } from "./columnTypeComp"; import { ColorControl } from "comps/controls/colorControl"; import { JSONValue } from "util/jsonTypes"; @@ -120,7 +120,7 @@ const StyledBorderRadiusIcon = styled(IconRadius)` width: 24px; margin: 0 8px 0 const StyledBorderIcon = styled(BorderWidthIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; const StyledTextSizeIcon = styled(TextSizeIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; const StyledFontFamilyIcon = styled(FontFamilyIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; -const StyledTextWeightIcon = styled(TextWeigthIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; +const StyledTextWeightIcon = styled(TextWeightIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; const StyledBackgroundImageIcon = styled(ImageCompIcon)` width: 24px; margin: 0 0px 0 -12px;`; /** @@ -303,7 +303,7 @@ export class ColumnComp extends ColumnInitComp { })} {this.children.textWeight.propertyView({ label: trans('style.textWeight'), - preInputNode: , + preInputNode: , placeholder: 'normal', })} {this.children.fontFamily.propertyView({ diff --git a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx index 7433cc64d..40e9b0f3f 100644 --- a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx @@ -46,7 +46,7 @@ import { import { timelineDate, timelineNode, TimelineDataTooltip } from "./timelineConstants"; import { convertTimeLineData } from "./timelineUtils"; import { default as Timeline } from "antd/es/timeline"; -import { EditorContext } from "comps/editorState"; +import { EditorContext } from "comps/editorState"; const EventOptions = [ clickEvent, @@ -108,7 +108,7 @@ const TimelineComp = ( color: value?.color, dot: icons[index] || "", label: ( - + {value?.label} ), diff --git a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineConstants.tsx b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineConstants.tsx index c819ce20b..3f4cd9d90 100644 --- a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineConstants.tsx @@ -7,7 +7,7 @@ export type timelineNode = { dot?: string; subTitleColor?: string; titleColor?: string; - lableColor?: string; + labelColor?: string; }; export const TimelineDataTooltip = ( @@ -28,7 +28,7 @@ export const TimelineDataTooltip = (
7. subTitleColor - {trans("timeLine.helpSubTitleColor")}
- 8. lableColor - {trans("timeLine.helpLableColor")} + 8. labelColor - {trans("timeLine.helpLabelColor")} ); @@ -51,7 +51,7 @@ export const timelineDate=[ color: 'red', titleColor: "red", subTitleColor: "red", - lableColor: "red", + labelColor: "red", }, { title: "Lowcoder 2.0", @@ -60,4 +60,4 @@ export const timelineDate=[ color: "green", label: "2023-6-20", }, -] \ No newline at end of file +] diff --git a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineUtils.tsx b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineUtils.tsx index f0657afa2..1e748c0f6 100644 --- a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineUtils.tsx @@ -15,7 +15,7 @@ function checkDataNodes(value: any, key?: string): timelineNode[] | undefined { check(node["color"], ["string", "undefined"], "color"); check(node["titleColor"], ["string", "undefined"], "titleColor"); check(node["subTitleColor"], ["string", "undefined"], "subTitleColor"); - check(node["lableColor"], ["string", "undefined"], "lableColor"); + check(node["labelColor"], ["string", "undefined"], "labelColor"); check(node["dot"], ["string", "undefined"], "dot"); return node; }); diff --git a/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx b/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx index d1d57401a..76e2dae74 100644 --- a/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx @@ -9,12 +9,12 @@ import { gridItemCompToGridItems, InnerGrid } from "../containerComp/containerVi import { TriContainerViewProps } from "../triContainerComp/triContainerCompBuilder"; const getStyle = (style: ContainerStyleType) => { - return css` + return css` border-color: ${style.border}; border-width: ${style.borderWidth}; border-radius: ${style.radius}; overflow: hidden; - padding: ${style.padding}; + padding: ${style.padding}; ${style.background && `background-color: ${style.background};`} ${style.backgroundImage && `background-image: ${style.backgroundImage};`} ${style.backgroundImageRepeat && `background-repeat: ${style.backgroundImageRepeat};`} @@ -33,7 +33,7 @@ const Wrapper = styled.div<{ $style: ContainerStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; -const HeaderInnerGrid = styled(InnerGrid)<{ +const HeaderInnerGrid = styled(InnerGrid)<{ $backgroundColor: string $headerBackgroundImage: string; $headerBackgroundImageRepeat: string; @@ -140,7 +140,7 @@ export function TriContainer(props: TriContainerProps) { $headerBackgroundImageSize={headerStyle?.headerBackgroundImageSize} $headerBackgroundImagePosition={headerStyle?.headerBackgroundImagePosition} $headerBackgroundImageOrigin={headerStyle?.headerBackgroundImageOrigin} - style={{padding: headerStyle.containerheaderpadding}} + style={{padding: headerStyle.containerHeaderPadding}} /> @@ -168,7 +168,7 @@ export function TriContainer(props: TriContainerProps) { $backgroundImageSize={bodyStyle?.backgroundImageSize} $backgroundImagePosition={bodyStyle?.backgroundImagePosition} $backgroundImageOrigin={bodyStyle?.backgroundImageOrigin} - style={{padding: bodyStyle.containerbodypadding}} + style={{padding: bodyStyle.containerBodyPadding}} /> ) : ( @@ -191,7 +191,7 @@ export function TriContainer(props: TriContainerProps) { $backgroundImageSize={bodyStyle?.backgroundImageSize} $backgroundImagePosition={bodyStyle?.backgroundImagePosition} $backgroundImageOrigin={bodyStyle?.backgroundImageOrigin} - style={{padding: bodyStyle.containerbodypadding}}/> + style={{padding: bodyStyle.containerBodyPadding}}/> )} )} @@ -214,7 +214,7 @@ export function TriContainer(props: TriContainerProps) { $footerBackgroundImageOrigin={footerStyle?.footerBackgroundImageOrigin} $borderColor={style?.border} $borderWidth={style?.borderWidth} - style={{padding: footerStyle.containerfooterpadding}} + style={{padding: footerStyle.containerFooterPadding}} /> )} diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index 880b1a69d..ef7b0813d 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -14,7 +14,7 @@ import { CompressIcon, TextSizeIcon, FontFamilyIcon, - TextWeigthIcon, + TextWeightIcon, ShowBorderIcon, BorderWidthIcon, ImageCompIcon, @@ -569,7 +569,7 @@ const MarginIcon = styled(ExpandIcon)` margin: 0 8px 0 2px;`; const PaddingIcon = styled(CompressIcon)` margin: 0 8px 0 2px;`; const StyledTextSizeIcon = styled(TextSizeIcon)` margin: 0 8px 0 -3px; padding: 3px;`; const StyledFontFamilyIcon = styled(FontFamilyIcon)` margin: 0 8px 0 -3px; padding: 3px;`; -const StyledTextWeightIcon = styled(TextWeigthIcon)` margin: 0 8px 0 -3px; padding: 3px;`; +const StyledTextWeightIcon = styled(TextWeightIcon)` margin: 0 8px 0 -3px; padding: 3px;`; const StyledBackgroundImageIcon = styled(ImageCompIcon)` margin: 0 0px 0 -12px;`; const ResetIcon = styled(IconReset)` @@ -611,9 +611,9 @@ export function styleControl(colorConfig name === "footerBackgroundImageOrigin" || name === "margin" || name === "padding" || - name === "containerheaderpadding" || - name === "containerfooterpadding" || - name === "containerbodypadding" + name === "containerHeaderPadding" || + name === "containerFooterPadding" || + name === "containerBodyPadding" ) { childrenMap[name] = StringControl; } else { @@ -657,9 +657,9 @@ export function styleControl(colorConfig name === "radius" || name === "margin" || name === "padding" || - name === "containerheaderpadding" || - name === "containerfooterpadding" || - name === "containerbodypadding" || + name === "containerHeaderPadding" || + name === "containerFooterPadding" || + name === "containerBodyPadding" || name === "borderWidth" || name === "backgroundImage" || name === "backgroundImageRepeat" || @@ -748,9 +748,9 @@ export function styleControl(colorConfig placeholder: props[name], }) : (name === "padding" || - name === "containerheaderpadding" || - name === "containerfooterpadding" || - name === "containerbodypadding") + name === "containerHeaderPadding" || + name === "containerFooterPadding" || + name === "containerBodyPadding") ? ( children[name] as InstanceType ).propertyView({ @@ -847,7 +847,7 @@ export function styleControl(colorConfig : children[name].propertyView({ label: config.label, panelDefaultColor: props[name], - // isDep: isDepColorConfig(config), + // isDep: isDepColorConfig(config), isDep: true, depMsg: depMsg, })} @@ -870,4 +870,4 @@ export function useStyle(colorConfigs: T props[config.name as Names] = ""; }); return calcColors(props, colorConfigs, theme?.theme, bgColor); -} \ No newline at end of file +} diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 5aeec739c..6e4297907 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -10,9 +10,11 @@ type CommonColorConfig = { readonly label: string; readonly platform?: SupportPlatform; // support all if undefined }; + export type SimpleColorConfig = CommonColorConfig & { readonly color: string; }; + export type RadiusConfig = CommonColorConfig & { readonly radius: string; }; @@ -59,21 +61,22 @@ export type borderStyleConfig = CommonColorConfig & { readonly borderStyle: string; } -export type ContainerHeaderPaddigConfig = CommonColorConfig & { - readonly containerheaderpadding: string; +export type ContainerHeaderPaddingConfig = CommonColorConfig & { + readonly containerHeaderPadding: string; }; -export type ContainerBodyPaddigConfig = CommonColorConfig & { - readonly containerbodypadding: string; +export type ContainerBodyPaddingConfig = CommonColorConfig & { + readonly containerBodyPadding: string; }; -export type ContainerFooterPaddigConfig = CommonColorConfig & { - readonly containerfooterpadding: string; +export type ContainerFooterPaddingConfig = CommonColorConfig & { + readonly containerFooterPadding: string; }; export type MarginConfig = CommonColorConfig & { readonly margin: string; }; + export type PaddingConfig = CommonColorConfig & { readonly padding: string; }; @@ -92,7 +95,8 @@ export type DepColorConfig = CommonColorConfig & { readonly depType?: DEP_TYPE; transformer: (color: string, ...rest: string[]) => string; }; -export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | borderStyleConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; + +export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | borderStyleConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddingConfig | ContainerFooterPaddingConfig | ContainerBodyPaddingConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; export const defaultTheme: ThemeDetail = { primary: "#3377FF", @@ -152,7 +156,7 @@ export function backgroundToBorder(color: string) { return darkenColor(color, 0.03); } -// calendar background color to boder +// calendar background color to border export function calendarBackgroundToBorder(color: string) { if (toHex(color) === SURFACE_COLOR) { return SECOND_SURFACE_COLOR; @@ -222,7 +226,7 @@ function handleCalendarSelectColor(color: string) { } // return lighten color -function handlelightenColor(color: string) { +function handleLightenColor(color: string) { return lightenColor(color, 0.1); } @@ -380,23 +384,22 @@ const FONT_STYLE = { fontStyle: "fontStyle", } as const -const CONTAINERHEADERPADDING = { - name: "containerheaderpadding", - label: trans("style.containerheaderpadding"), - containerheaderpadding: "padding", +const CONTAINER_HEADER_PADDING = { + name: "containerHeaderPadding", + label: trans("style.containerHeaderPadding"), + containerHeaderPadding: "padding", } as const; -const CONTAINERFOOTERPADDING = { - name: "containerfooterpadding", - label: trans("style.containerfooterpadding"), - containerfooterpadding: "padding", +const CONTAINER_FOOTER_PADDING = { + name: "containerFooterPadding", + label: trans("style.containerFooterPadding"), + containerFooterPadding: "padding", } as const; - -const CONTAINERBODYPADDING = { - name: "containerbodypadding", - label: trans("style.containerbodypadding"), - containerbodypadding: "padding", +const CONTAINER_BODY_PADDING = { + name: "containerBodyPadding", + label: trans("style.containerBodyPadding"), + containerBodyPadding: "padding", } as const; const TEXT_TRANSFORM = { @@ -499,7 +502,6 @@ function getStaticBackground(color: string) { } as const; } - function replaceAndMergeMultipleStyles(originalArray: any[], styleToReplace: string, replacingStyles: any[]): any[] { let temp = [] let foundIndex = originalArray.findIndex((element) => element.name === styleToReplace) @@ -561,7 +563,6 @@ export const MarginStyle = [ }, ]; - export const ContainerStyle = [ // ...BG_STATIC_BORDER_RADIUS, getStaticBorder(), @@ -599,7 +600,7 @@ export const ContainerStyle = [ ] as const; export const ContainerHeaderStyle = [ - CONTAINERHEADERPADDING, + CONTAINER_HEADER_PADDING, HEADER_BACKGROUND, { name: "headerBackgroundImage", @@ -629,7 +630,7 @@ export const ContainerHeaderStyle = [ ] as const; export const ContainerBodyStyle = [ - CONTAINERBODYPADDING, + CONTAINER_BODY_PADDING, { name: "background", label: trans("style.background"), @@ -665,7 +666,7 @@ export const ContainerBodyStyle = [ ] as const; export const ContainerFooterStyle = [ - CONTAINERFOOTERPADDING, + CONTAINER_FOOTER_PADDING, { name: "footerBackground", label: trans("style.background"), @@ -703,7 +704,7 @@ export const ContainerFooterStyle = [ export const SliderStyle = [ FILL, { - name: "thumbBoder", + name: "thumbBorder", label: trans("style.thumbBorder"), depName: "fill", depType: DEP_TYPE.SELF, @@ -1032,7 +1033,7 @@ export const TableColumnLinkStyle = [ ] as const; export const FileStyle = [ - // ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), + // ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), getStaticBackground(SURFACE_COLOR), ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'border', [getStaticBorder('#00000000')]), // TEXT, ACCENT, MARGIN, PADDING @@ -1154,7 +1155,6 @@ export const IconStyle = [ MARGIN, PADDING] as const; - export const ListViewStyle = BG_STATIC_BORDER_RADIUS; export const JsonSchemaFormStyle = BG_STATIC_BORDER_RADIUS; @@ -1181,8 +1181,8 @@ export const TimeLineStyle = [ color: "#000000", }, { - name: "lableColor", - label: trans("timeLine.lableColor"), + name: "labelColor", + label: trans("timeLine.labelColor"), color: "#000000", }, { @@ -1227,7 +1227,7 @@ export const CalendarStyle = [ name: "headerBtnBackground", label: trans("calendar.headerBtnBackground"), depName: "background", - transformer: handlelightenColor, + transformer: handleLightenColor, }, { name: "btnText", @@ -1285,7 +1285,7 @@ export const LottieStyle = [ MARGIN, PADDING, ] as const; -///////////////////// + export const CommentStyle = [ { name: "background", @@ -1298,6 +1298,7 @@ export const CommentStyle = [ PADDING, RADIUS, ] as const + export const ResponsiveLayoutRowStyle = [ ...BG_STATIC_BORDER_RADIUS, MARGIN, @@ -1469,5 +1470,5 @@ export function marginCalculator(margin: string) { return parseInt(marginArr[0]?.replace(/[^\d.]/g, "") || "0") + parseInt(marginArr[2]?.replace(/[^\d.]/g, "") || "0") } } -export type { ThemeDetail }; +export type { ThemeDetail }; diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index b2f752f51..240173dd2 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -284,7 +284,7 @@ export const de = { "marginDesc": "Standardmarge, die typischerweise für die meisten Komponenten verwendet wird", "padding": "Polsterung", "paddingDesc": "Standardpolsterung, die für die meisten Komponenten verwendet wird", - "containerheaderpadding": "Kopfzeilenpolsterung", + "containerHeaderPadding": "Kopfzeilenpolsterung", "containerheaderpaddingDesc": "Standard-Header-Padding, das für die meisten Komponenten verwendet wird", "gridColumns": "Rasterspalten", "gridColumnsDesc": "Standardanzahl von Spalten, die für die meisten Container verwendet wird" @@ -342,9 +342,9 @@ export const de = { "marginRight": "Rand rechts", "marginTop": "Marge oben", "marginBottom": "Marge Unten", - "containerheaderpadding": "Kopfzeilenpolsterung", - "containerfooterpadding": "Fußzeilenpolsterung", - "containerbodypadding": "Körperpolsterung", + "containerHeaderPadding": "Kopfzeilenpolsterung", + "containerFooterPadding": "Fußzeilenpolsterung", + "containerBodyPadding": "Körperpolsterung", "minWidth": "Mindestbreite", "aspectRatio": "Seitenverhältnis", "textSize": "Textgröße" @@ -2451,7 +2451,7 @@ export const de = { "timeLine": { "titleColor": "Titel Farbe", "subTitleColor": "Untertitel Farbe", - "lableColor": "Etikett Farbe", + "labelColor": "Etikett Farbe", "value": "Zeitleiste Daten", "mode": "Bestellung anzeigen", "left": "Inhalt Rechts", @@ -2472,7 +2472,7 @@ export const de = { "helpDot": "Timeline-Knoten als Ameisendesign-Symbole rendern", "helpTitleColor": "Individuell die Farbe des Knotentitels steuern", "helpSubTitleColor": "Individuelle Steuerung der Farbe der Knotenuntertitel", - "helpLableColor": "Individuelle Steuerung der Farbe des Knotensymbols", + "helpLabelColor": "Individuelle Steuerung der Farbe des Knotensymbols", "valueDesc": "Daten der Zeitleiste", "clickedObjectDesc": "Daten zum angeklickten Objekt", "clickedIndexDesc": "Index der angeklickten Objekte" @@ -2558,4 +2558,4 @@ export const de = { "navStyle": "Menü-Stil", "navItemStyle": "Menüpunkt Stil" } -}; \ No newline at end of file +}; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 4837b7371..573d33234 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -114,7 +114,7 @@ export const en = { // second part - + "bottomPanel": { "title": "Data Queries", "run": "Run", @@ -302,7 +302,7 @@ export const en = { "marginDesc": "Default margin typically used for most components", "padding": "Padding", "paddingDesc": "Default padding typically used for most components", - "containerheaderpadding": "Header Padding", + "containerHeaderPadding": "Header Padding", "containerheaderpaddingDesc": "Default header padding typically used for most components", "gridColumns": "Grid Columns", "gridColumnsDesc": "Default number of columns typically used for most containers" @@ -363,9 +363,9 @@ export const en = { "marginRight": "Margin Right", "marginTop": "Margin Top", "marginBottom": "Margin Bottom", - "containerheaderpadding": "Header Padding", - "containerfooterpadding": "Footer Padding", - "containerbodypadding": "Body Padding", + "containerHeaderPadding": "Header Padding", + "containerFooterPadding": "Footer Padding", + "containerBodyPadding": "Body Padding", "minWidth": "Minimum Width", "aspectRatio": "Aspect Ratio", "textSize": "Text Size", @@ -702,119 +702,119 @@ export const en = { "autoCompleteCompName": "Auto Complete", "autoCompleteCompDesc": "An input field that provides suggestions as you type, enhancing user experience and accuracy.", "autoCompleteCompKeywords": "suggestions, autocomplete, typing, input", - + "inputCompName": "Input", "inputCompDesc": "A basic text input field allowing users to enter and edit text.", "inputCompKeywords": "text, input, field, edit", - + "textAreaCompName": "Text Area", "textAreaCompDesc": "A multi-line text input for longer form content, such as comments or descriptions.", "textAreaCompKeywords": "multiline, textarea, input, text", - + "passwordCompName": "Password", "passwordCompDesc": "A secure field for password input, masking the characters for privacy.", "passwordCompKeywords": "password, security, input, hidden", - + "richTextEditorCompName": "Rich Text Editor", "richTextEditorCompDesc": "An advanced text editor supporting rich formatting options like bold, italics, and lists.", "richTextEditorCompKeywords": "editor, text, formatting, rich content", - + "numberInputCompName": "Number Input", "numberInputCompDesc": "A field specifically for numerical input, with controls for incrementing and decrementing values.", "numberInputCompKeywords": "number, input, increment, decrement", - + "sliderCompName": "Slider", "sliderCompDesc": "A graphical slider component for selecting a value or range within a defined scale.", "sliderCompKeywords": "slider, range, input, graphical", - + "rangeSliderCompName": "Range Slider", "rangeSliderCompDesc": "A dual-handle slider to select a range of values, useful for filtering or setting limits.", "rangeSliderCompKeywords": "range, slider, dual-handle, filter", - + "ratingCompName": "Rating", "ratingCompDesc": "A component for capturing user ratings, displayed as stars.", "ratingCompKeywords": "rating, stars, feedback, input", - + "switchCompName": "Switch", "switchCompDesc": "A toggle switch for on/off or yes/no type decisions.", "switchCompKeywords": "toggle, switch, on/off, control", - + "selectCompName": "Select", "selectCompDesc": "A dropdown menu for selecting from a list of options.", "selectCompKeywords": "dropdown, select, options, menu", - + "multiSelectCompName": "Multiselect", "multiSelectCompDesc": "A component that allows selection of multiple items from a dropdown list.", "multiSelectCompKeywords": "multiselect, multiple, dropdown, choices", - + "cascaderCompName": "Cascader", "cascaderCompDesc": "A multi-level dropdown for hierarchical data selection, such as selecting a location.", "cascaderCompKeywords": "cascader, hierarchical, dropdown, levels", - + "checkboxCompName": "Checkbox", "checkboxCompDesc": "A standard checkbox for options that can be selected or deselected.", "checkboxCompKeywords": "checkbox, options, select, toggle", - + "radioCompName": "Radio", "radioCompDesc": "Radio buttons for selecting one option from a set, where only one choice is allowed.", "radioCompKeywords": "radio, buttons, select, single choice", - + "segmentedControlCompName": "Segmented Control", "segmentedControlCompDesc": "A control with segmented options for quickly toggling between multiple choices.", "segmentedControlCompKeywords": "segmented, control, toggle, options", - + "fileUploadCompName": "File Upload", "fileUploadCompDesc": "A component for uploading files, with support for drag-and-drop and file selection.", "fileUploadCompKeywords": "file, upload, drag and drop, select", - + "dateCompName": "Date", "dateCompDesc": "A date picker component for selecting dates from a calendar interface.", "dateCompKeywords": "date, picker, calendar, select", - + "dateRangeCompName": "Date Range", "dateRangeCompDesc": "A component for selecting a range of dates, useful for booking systems or filters.", "dateRangeCompKeywords": "daterange, select, booking, filter", - + "timeCompName": "Time", "timeCompDesc": "A time selection component for choosing specific times of the day.", "timeCompKeywords": "time, picker, select, clock", - + "timeRangeCompName": "Time Range", "timeRangeCompDesc": "A component for selecting a range of time, often used in scheduling applications.", "timeRangeCompKeywords": "timerange, select, scheduling, duration", - + "buttonCompName": "Form Button", "buttonCompDesc": "A versatile button component for submitting forms, triggering actions, or navigating.", "buttonCompKeywords": "button, submit, action, navigate", - + "linkCompName": "Link", "linkCompDesc": "A hyperlink display component for navigation or linking to external resources.", "linkCompKeywords": "link, hyperlink, navigation, external", - + "scannerCompName": "Scanner", "scannerCompDesc": "A component for scanning barcodes, QR codes, and other similar data.", "scannerCompKeywords": "scanner, barcode, QR code, scan", - + "dropdownCompName": "Dropdown", "dropdownCompDesc": "A dropdown menu for compactly displaying a list of options.", "dropdownCompKeywords": "dropdown, menu, options, select", - + "toggleButtonCompName": "Toggle Button", "toggleButtonCompDesc": "A button that can toggle between two states or options.", "toggleButtonCompKeywords": "toggle, button, switch, state", - + "textCompName": "Text Display", "textCompDesc": "A simple component for displaying static or dynamic text content inclusive Markdown formatting.", "textCompKeywords": "text, display, static, dynamic", - + "tableCompName": "Table", "tableCompDesc": "A rich table component for displaying data in a structured table format, with options for sorting and filtering, tree Data display and extensible Rows.", "tableCompKeywords": "table, data, sorting, filtering", - + "imageCompName": "Image", "imageCompDesc": "A component for displaying images, supporting various formats based on URI or Base64 Data.", "imageCompKeywords": "image, display, media, Base64", - + "progressCompName": "Progress", "progressCompDesc": "A visual indicator of progress, typically used to show the completion status of a task.", "progressCompKeywords": "progress, indicator, status, task", @@ -2133,7 +2133,7 @@ export const en = { // eighteenth part - + "help": { "videoText": "Overview", @@ -2674,7 +2674,7 @@ export const en = { "timeLine": { "titleColor": "Title Color", "subTitleColor": "Subtitle Color", - "lableColor": "Label Color", + "labelColor": "Label Color", "value": "Timeline Data", "mode": "Display Order", "left": "Content Right", @@ -2695,7 +2695,7 @@ export const en = { "helpDot": "Rendering Timeline Nodes as Ant Design Icons", "helpTitleColor": "Individually Control the Color of Node Title", "helpSubTitleColor": "Individually Control the Color of Node Subtitle", - "helpLableColor": "Individually Control the Color of Node Icon", + "helpLabelColor": "Individually Control the Color of Node Icon", "valueDesc": "Data of Timeline", "clickedObjectDesc": "Clicked Item Data", "clickedIndexDesc": "Clicked Item Index" @@ -2837,4 +2837,4 @@ export const en = { // If you want to write this to a file, you can use the fs module // const fs = require('fs'); -// fs.writeFileSync('output.json', jsonString, 'utf8'); \ No newline at end of file +// fs.writeFileSync('output.json', jsonString, 'utf8'); diff --git a/client/packages/lowcoder/src/i18n/locales/translation_files/de.json b/client/packages/lowcoder/src/i18n/locales/translation_files/de.json index 5b64c4f81..c19608812 100644 --- a/client/packages/lowcoder/src/i18n/locales/translation_files/de.json +++ b/client/packages/lowcoder/src/i18n/locales/translation_files/de.json @@ -282,7 +282,7 @@ "marginDesc": "Standardmarge, die typischerweise für die meisten Komponenten verwendet wird", "padding": "Polsterung", "paddingDesc": "Standardpolsterung, die für die meisten Komponenten verwendet wird", - "containerheaderpadding": "Kopfzeilenpolsterung", + "containerHeaderPadding": "Kopfzeilenpolsterung", "containerheaderpaddingDesc": "Standard-Header-Padding, das für die meisten Komponenten verwendet wird", "gridColumns": "Rasterspalten", "gridColumnsDesc": "Standardanzahl von Spalten, die für die meisten Container verwendet wird" @@ -336,9 +336,9 @@ "marginRight": "Rand rechts", "marginTop": "Marge oben", "marginBottom": "Marge Unten", - "containerheaderpadding": "Kopfzeilenpolsterung", - "containerfooterpadding": "Fußzeilenpolsterung", - "containerbodypadding": "Körperpolsterung", + "containerHeaderPadding": "Kopfzeilenpolsterung", + "containerFooterPadding": "Fußzeilenpolsterung", + "containerBodyPadding": "Körperpolsterung", "minWidth": "Mindestbreite", "aspectRatio": "Seitenverhältnis", "textSize": "Textgröße" @@ -2419,7 +2419,7 @@ "timeLine": { "titleColor": "Titel Farbe", "subTitleColor": "Untertitel Farbe", - "lableColor": "Etikett Farbe", + "labelColor": "Etikett Farbe", "value": "Zeitleiste Daten", "mode": "Bestellung anzeigen", "left": "Inhalt Rechts", @@ -2440,7 +2440,7 @@ "helpDot": "Timeline-Knoten als Ameisendesign-Symbole rendern", "helpTitleColor": "Individuell die Farbe des Knotentitels steuern", "helpSubTitleColor": "Individuelle Steuerung der Farbe der Knotenuntertitel", - "helpLableColor": "Individuelle Steuerung der Farbe des Knotensymbols", + "helpLabelColor": "Individuelle Steuerung der Farbe des Knotensymbols", "valueDesc": "Daten der Zeitleiste", "clickedObjectDesc": "Daten zum angeklickten Objekt", "clickedIndexDesc": "Index der angeklickten Objekte" @@ -2526,4 +2526,4 @@ "navStyle": "Menü-Stil", "navItemStyle": "Menüpunkt Stil" } -} \ No newline at end of file +} diff --git a/client/packages/lowcoder/src/i18n/locales/translation_files/en.json b/client/packages/lowcoder/src/i18n/locales/translation_files/en.json index 279c51ae1..2fc7a70ec 100644 --- a/client/packages/lowcoder/src/i18n/locales/translation_files/en.json +++ b/client/packages/lowcoder/src/i18n/locales/translation_files/en.json @@ -283,7 +283,7 @@ "marginDesc": "Default margin typically used for most components", "padding": "Padding", "paddingDesc": "Default padding typically used for most components", - "containerheaderpadding": "Header Padding", + "containerHeaderPadding": "Header Padding", "containerheaderpaddingDesc": "Default header padding typically used for most components", "gridColumns": "Grid Columns", "gridColumnsDesc": "Default number of columns typically used for most containers" @@ -337,9 +337,9 @@ "marginRight": "Margin Right", "marginTop": "Margin Top", "marginBottom": "Margin Bottom", - "containerheaderpadding": "Header Padding", - "containerfooterpadding": "Footer Padding", - "containerbodypadding": "Body Padding", + "containerHeaderPadding": "Header Padding", + "containerFooterPadding": "Footer Padding", + "containerBodyPadding": "Body Padding", "minWidth": "Minimum Width", "aspectRatio": "Aspect Ratio", "textSize": "Text Size" @@ -648,119 +648,119 @@ "autoCompleteCompName": "Auto Complete", "autoCompleteCompDesc": "An input field that provides suggestions as you type, enhancing user experience and accuracy.", "autoCompleteCompKeywords": "suggestions, autocomplete, typing, input", - + "inputCompName": "Input", "inputCompDesc": "A basic text input field allowing users to enter and edit text.", "inputCompKeywords": "text, input, field, edit", - + "textAreaCompName": "Text Area", "textAreaCompDesc": "A multi-line text input for longer form content, such as comments or descriptions.", "textAreaCompKeywords": "multiline, textarea, input, text", - + "passwordCompName": "Password", "passwordCompDesc": "A secure field for password input, masking the characters for privacy.", "passwordCompKeywords": "password, security, input, hidden", - + "richTextEditorCompName": "Rich Text Editor", "richTextEditorCompDesc": "An advanced text editor supporting rich formatting options like bold, italics, and lists.", "richTextEditorCompKeywords": "editor, text, formatting, rich content", - + "numberInputCompName": "Number Input", "numberInputCompDesc": "A field specifically for numerical input, with controls for incrementing and decrementing values.", "numberInputCompKeywords": "number, input, increment, decrement", - + "sliderCompName": "Slider", "sliderCompDesc": "A graphical slider component for selecting a value or range within a defined scale.", "sliderCompKeywords": "slider, range, input, graphical", - + "rangeSliderCompName": "Range Slider", "rangeSliderCompDesc": "A dual-handle slider to select a range of values, useful for filtering or setting limits.", "rangeSliderCompKeywords": "range, slider, dual-handle, filter", - + "ratingCompName": "Rating", "ratingCompDesc": "A component for capturing user ratings, displayed as stars.", "ratingCompKeywords": "rating, stars, feedback, input", - + "switchCompName": "Switch", "switchCompDesc": "A toggle switch for on/off or yes/no type decisions.", "switchCompKeywords": "toggle, switch, on/off, control", - + "selectCompName": "Select", "selectCompDesc": "A dropdown menu for selecting from a list of options.", "selectCompKeywords": "dropdown, select, options, menu", - + "multiSelectCompName": "Multiselect", "multiSelectCompDesc": "A component that allows selection of multiple items from a dropdown list.", "multiSelectCompKeywords": "multiselect, multiple, dropdown, choices", - + "cascaderCompName": "Cascader", "cascaderCompDesc": "A multi-level dropdown for hierarchical data selection, such as selecting a location.", "cascaderCompKeywords": "cascader, hierarchical, dropdown, levels", - + "checkboxCompName": "Checkbox", "checkboxCompDesc": "A standard checkbox for options that can be selected or deselected.", "checkboxCompKeywords": "checkbox, options, select, toggle", - + "radioCompName": "Radio", "radioCompDesc": "Radio buttons for selecting one option from a set, where only one choice is allowed.", "radioCompKeywords": "radio, buttons, select, single choice", - + "segmentedControlCompName": "Segmented Control", "segmentedControlCompDesc": "A control with segmented options for quickly toggling between multiple choices.", "segmentedControlCompKeywords": "segmented, control, toggle, options", - + "fileUploadCompName": "File Upload", "fileUploadCompDesc": "A component for uploading files, with support for drag-and-drop and file selection.", "fileUploadCompKeywords": "file, upload, drag and drop, select", - + "dateCompName": "Date", "dateCompDesc": "A date picker component for selecting dates from a calendar interface.", "dateCompKeywords": "date, picker, calendar, select", - + "dateRangeCompName": "Date Range", "dateRangeCompDesc": "A component for selecting a range of dates, useful for booking systems or filters.", "dateRangeCompKeywords": "daterange, select, booking, filter", - + "timeCompName": "Time", "timeCompDesc": "A time selection component for choosing specific times of the day.", "timeCompKeywords": "time, picker, select, clock", - + "timeRangeCompName": "Time Range", "timeRangeCompDesc": "A component for selecting a range of time, often used in scheduling applications.", "timeRangeCompKeywords": "timerange, select, scheduling, duration", - + "buttonCompName": "Form Button", "buttonCompDesc": "A versatile button component for submitting forms, triggering actions, or navigating.", "buttonCompKeywords": "button, submit, action, navigate", - + "linkCompName": "Link", "linkCompDesc": "A hyperlink display component for navigation or linking to external resources.", "linkCompKeywords": "link, hyperlink, navigation, external", - + "scannerCompName": "Scanner", "scannerCompDesc": "A component for scanning barcodes, QR codes, and other similar data.", "scannerCompKeywords": "scanner, barcode, QR code, scan", - + "dropdownCompName": "Dropdown", "dropdownCompDesc": "A dropdown menu for compactly displaying a list of options.", "dropdownCompKeywords": "dropdown, menu, options, select", - + "toggleButtonCompName": "Toggle Button", "toggleButtonCompDesc": "A button that can toggle between two states or options.", "toggleButtonCompKeywords": "toggle, button, switch, state", - + "textCompName": "Text Display", "textCompDesc": "A simple component for displaying static or dynamic text content inclusive Markdown formatting.", "textCompKeywords": "text, display, static, dynamic", - + "tableCompName": "Table", "tableCompDesc": "A rich table component for displaying data in a structured table format, with options for sorting and filtering, tree Data display and extensible Rows.", "tableCompKeywords": "table, data, sorting, filtering", - + "imageCompName": "Image", "imageCompDesc": "A component for displaying images, supporting various formats based on URI or Base64 Data.", "imageCompKeywords": "image, display, media, Base64", - + "progressCompName": "Progress", "progressCompDesc": "A visual indicator of progress, typically used to show the completion status of a task.", "progressCompKeywords": "progress, indicator, status, task", @@ -2484,7 +2484,7 @@ "timeLine": { "titleColor": "Title Color", "subTitleColor": "Subtitle Color", - "lableColor": "Label Color", + "labelColor": "Label Color", "value": "Timeline Data", "mode": "Display Order", "left": "Content Right", @@ -2505,7 +2505,7 @@ "helpDot": "Rendering Timeline Nodes as Ant Design Icons", "helpTitleColor": "Individually Control the Color of Node Title", "helpSubTitleColor": "Individually Control the Color of Node Subtitle", - "helpLableColor": "Individually Control the Color of Node Icon", + "helpLabelColor": "Individually Control the Color of Node Icon", "valueDesc": "Data of Timeline", "clickedObjectDesc": "Clicked Item Data", "clickedIndexDesc": "Clicked Item Index" @@ -2591,4 +2591,4 @@ "navStyle": "Menu Style", "navItemStyle": "Menu Item Style" } -} \ No newline at end of file +} diff --git a/client/packages/lowcoder/src/i18n/locales/translation_files/es.json b/client/packages/lowcoder/src/i18n/locales/translation_files/es.json index 1a2e8e409..1beecfd47 100644 --- a/client/packages/lowcoder/src/i18n/locales/translation_files/es.json +++ b/client/packages/lowcoder/src/i18n/locales/translation_files/es.json @@ -1 +1 @@ -{"productName":"Lowcoder","productDesc":"Crea aplicaciones de software para tu empresa y tus clientes con una experiencia mínima en codificación. Lowcoder es una excelente alternativa a Retool, Appsmith y Tooljet.","notSupportedBrowser":"Tu navegador actual puede tener problemas de compatibilidad. Para una experiencia de usuario óptima, utiliza la última versión de Chrome.","create":"Crea","move":"Muévete","addItem":"Añade","newItem":"Nuevo","copy":"Copia","rename":"Cambia el nombre de","delete":"Borra","deletePermanently":"Borrar permanentemente","remove":"Elimina","recover":"Recupera","edit":"Edita","view":"Ver","value":"Valor","data":"Datos","information":"Información","success":"Éxito","warning":"Advertencia","error":"Error","reference":"Referencia","text":"Texto","label":"Etiqueta","color":"Color","form":"Formulario","menu":"Menú","menuItem":"Elemento del menú","ok":"OK","cancel":"Cancelar","finish":"Acabado","reset":"Restablece","icon":"Icono","code":"Código","title":"Título","emptyContent":"Contenido vacío","more":"Más","search":"Busca en","back":"Volver","accessControl":"Control de acceso","copySuccess":"Copiado correctamente","copyError":"Error de copia","api":{"publishSuccess":"Publicado con éxito","recoverFailed":"Recuperación fallida","needUpdate":"Tu versión actual está obsoleta. Por favor, actualízala a la última versión."},"codeEditor":{"notSupportAutoFormat":"El editor de código actual no admite el autoformateo.","fold":"Pliega"},"exportMethod":{"setDesc":"Establecer propiedad: {propiedad}","clearDesc":"Borrar propiedad: {propiedad}","resetDesc":"Restablecer propiedad: {propiedad} al valor por defecto"},"method":{"focus":"Focalizar","focusOptions":"Opciones de enfoque. Ver HTMLElement.focus()","blur":"Eliminar Enfoque","click":"Haz clic en","select":"Seleccionar todo el texto","setSelectionRange":"Establecer las posiciones inicial y final de la selección de texto","selectionStart":"Índice basado en 0 del primer carácter seleccionado","selectionEnd":"Índice basado en 0 del carácter después del último carácter seleccionado","setRangeText":"Reemplazar rango de texto","replacement":"Cadena a insertar","replaceStart":"Índice basado en 0 del primer carácter a sustituir","replaceEnd":"Índice basado en 0 del carácter después del último carácter a sustituir"},"errorBoundary":{"encounterError":"Error en la carga del componente. Comprueba tu configuración.","clickToReload":"Haz clic para recargar","errorMsg":"Error: "},"imgUpload":{"notSupportError":"Sólo admite tipos de imagen {tipos}","exceedSizeError":"El tamaño de la imagen no debe superar el {tamaño}."},"gridCompOperator":{"notSupport":"No admitido","selectAtLeastOneComponent":"Selecciona al menos un componente","selectCompFirst":"Seleccionar componentes antes de copiar","noContainerSelected":"[Bug] No se ha seleccionado ningún contenedor","deleteCompsSuccess":"Eliminado correctamente. Pulsa {Tecla Deshacer} para deshacer.","deleteCompsTitle":"Eliminar componentes","deleteCompsBody":"¿Estás seguro de que quieres borrar {compNum} componentes seleccionados?","cutCompsSuccess":"Corta con éxito. Pulsa {pegarTecla} para pegar, o {deshacerTecla} para deshacer."},"leftPanel":{"queries":"Consultas de datos en tu app","globals":"Variables de datos globales","propTipsArr":"{num} Elementos","propTips":"{num} Teclas","propTipArr":"{num} Artículo","propTip":"{num} Tecla","stateTab":"Estado","settingsTab":"Ajustes","toolbarTitle":"Individualización","toolbarPreload":"Guiones y estilos","components":"Componentes activos","modals":"Modales in-App","expandTip":"Haz clic para ampliar los datos del {componente}","collapseTip":"Haz clic para contraer los datos del {componente}"},"bottomPanel":{"title":"Consultas de datos","run":"Ejecuta","noSelectedQuery":"No se ha seleccionado ninguna consulta","metaData":"Metadatos de la fuente de datos","noMetadata":"No hay metadatos disponibles","metaSearchPlaceholder":"Buscar metadatos","allData":"Todas las mesas"},"rightPanel":{"propertyTab":"Propiedades","noSelectedComps":"No hay Componentes seleccionados. Haz clic en un Componente para ver sus Propiedades.","createTab":"Inserta","searchPlaceHolder":"Buscar componentes o módulos","uiComponentTab":"Componentes","extensionTab":"Extensiones","modulesTab":"Módulos","moduleListTitle":"Módulos","pluginListTitle":"Plugins","emptyModules":"Los módulos son Mikro-Apps reutilizables. Puedes incrustarlos en tu App.","searchNotFound":"¿No encuentras el componente adecuado? Envía un problema","emptyPlugins":"No se han añadido plugins","contactUs":"Contacta con nosotros","issueHere":"aquí."},"prop":{"expand":"Amplía","columns":"Columnas","videokey":"Llave de vídeo","rowSelection":"Selección de filas","toolbar":"Barra de herramientas","pagination":"Paginación","logo":"Logotipo","style":"Estilo","inputs":"Entradas","meta":"Metadatos","data":"Datos","hide":"Ocultar","loading":"Cargando","disabled":"Discapacitados","placeholder":"Marcador de posición","showClear":"Mostrar botón Borrar","showSearch":"Puedes buscar en","defaultValue":"Valor por defecto","required":"Campo obligatorio","readOnly":"Sólo lectura","readOnlyTooltip":"Los componentes de sólo lectura parecen normales, pero no se pueden modificar.","minimum":"Mínimo","maximum":"Máximo","regex":"Regex","minLength":"Longitud mínima","maxLength":"Longitud máxima","height":"Altura","width":"Anchura","selectApp":"Seleccionar aplicación","showCount":"Mostrar recuento","textType":"Tipo de texto","customRule":"Regla personalizada","customRuleTooltip":"Una cadena no vacía indica un error; vacía o nula significa que la validación se ha superado. Ejemplo: ","manual":"Manual","map":"Mapa","json":"JSON","use12Hours":"Utiliza el formato de 12 horas","hourStep":"Hora Paso","minuteStep":"Paso del minuto","secondStep":"Segundo paso","minDate":"Fecha mínima","maxDate":"Fecha máxima","minTime":"Tiempo mínimo","maxTime":"Tiempo máximo","type":"Tipo","showLabel":"Mostrar etiqueta","showHeader":"Mostrar cabecera","showBody":"Mostrar cuerpo","showFooter":"Mostrar pie de página","maskClosable":"Pulsa Fuera para Cerrar","showMask":"Mostrar máscara"},"autoHeightProp":{"auto":"Auto","fixed":"Fijo"},"labelProp":{"text":"Etiqueta","tooltip":"Información sobre herramientas","position":"Posición","left":"Izquierda","top":"Arriba","align":"Alineación","width":"Anchura","widthTooltip":"La anchura de la etiqueta admite porcentajes (%) y píxeles (px)."},"eventHandler":{"eventHandlers":"Manejadores de eventos","emptyEventHandlers":"Sin manejadores de eventos","incomplete":"Selección incompleta","inlineEventTitle":"En {eventName}","event":"Evento","action":"Acción","noSelect":"Sin selección","runQuery":"Ejecutar una consulta de datos","selectQuery":"Seleccionar consulta de datos","controlComp":"Controlar un componente","runScript":"Ejecutar JavaScript","runScriptPlaceHolder":"Escribe aquí el código","component":"Componente","method":"Método","setTempState":"Establece un valor de Estado Temporal","state":"Estado","triggerModuleEvent":"Activar un evento de módulo","moduleEvent":"Módulo Evento","goToApp":"Ir a otra App","queryParams":"Parámetros de consulta","hashParams":"Parámetros Hash","showNotification":"Mostrar una notificación","text":"Texto","level":"Nivel","duration":"Duración","notifyDurationTooltip":"La unidad de tiempo puede ser \\'s\\' (segundo, por defecto) o \\'ms\\' (milisegundo). La duración máxima es {max} segundos","goToURL":"Abrir una URL","openInNewTab":"Abrir en pestaña nueva","copyToClipboard":"Copiar un valor al Portapapeles","copyToClipboardValue":"Valor","export":"Exportar datos","exportNoFileType":"Sin selección (Opcional)","fileName":"Nombre del archivo","fileNameTooltip":"Incluye la extensión para especificar el tipo de archivo, por ejemplo: \\'imagen.png'","fileType":"Tipo de archivo","condition":"Corre sólo cuando...","conditionTooltip":"Ejecuta el controlador de eventos sólo cuando esta condición se evalúe como \"verdadero\".","debounce":"Rebote para","throttle":"Acelerador para","slowdownTooltip":"Utiliza debounce o throttle para controlar la frecuencia de los disparos de acción. La unidad de tiempo puede ser \\'ms\\' (milisegundo, por defecto) o \\'s\\' (segundo).","notHandledError":"No manipulado","currentApp":"Actual"},"event":{"submit":"Envía","submitDesc":"Activadores al enviar","change":"Cambia","changeDesc":"Activadores de cambios de valor","focus":"Enfoque","focusDesc":"Activadores en Foco","blur":"Desenfocar","blurDesc":"Activadores del desenfoque","click":"Haz clic en","clickDesc":"Activadores al hacer clic","close":"Cerrar","closeDesc":"Desencadenantes al cerrar","parse":"Analiza","parseDesc":"Disparadores en Parse","success":"Éxito","successDesc":"Activadores del éxito","delete":"Borra","deleteDesc":"Activadores al borrar","mention":"Menciona","mentionDesc":"Activadores de la mención"},"themeDetail":{"primary":"Color de la marca","primaryDesc":"Color primario por defecto utilizado por la mayoría de los componentes","textDark":"Color de texto oscuro","textDarkDesc":"Se utiliza cuando el color de fondo es claro","textLight":"Color de texto claro","textLightDesc":"Se utiliza cuando el color de fondo es oscuro","canvas":"Color del lienzo","canvasDesc":"Color de fondo predeterminado de la aplicación","primarySurface":"Color del envase","primarySurfaceDesc":"Color de fondo por defecto para componentes como las tablas","borderRadius":"Radio del borde","borderRadiusDesc":"Radio del borde por defecto utilizado por la mayoría de los componentes","chart":"Estilo gráfico","chartDesc":"Entrada para Echarts","echartsJson":"Tema JSON","margin":"Margen","marginDesc":"Margen por defecto utilizado normalmente para la mayoría de los componentes","padding":"Acolchado","paddingDesc":"Relleno por defecto utilizado normalmente para la mayoría de los componentes","containerheaderpadding":"Relleno de cabecera","containerheaderpaddingDesc":"Relleno de cabecera por defecto utilizado normalmente para la mayoría de los componentes","gridColumns":"Columnas de cuadrícula","gridColumnsDesc":"Número predeterminado de columnas utilizado normalmente para la mayoría de los contenedores"},"style":{"resetTooltip":"Restablecer estilos. Borra el campo de entrada para restablecer un estilo individual.","textColor":"Color del texto","contrastText":"Contraste Color del texto","generated":"Generado","customize":"Personaliza","staticText":"Texto estático","accent":"Acento","validate":"Mensaje de validación","border":"Color del borde","borderRadius":"Radio del borde","borderWidth":"Anchura del borde","background":"Antecedentes","headerBackground":"Fondo de la cabecera","footerBackground":"Fondo de pie de página","fill":"Rellena","track":"Pista","links":"Enlaces","thumb":"Pulgar","thumbBorder":"Borde del pulgar","checked":"Comprobado","unchecked":"Sin marcar","handle":"Asa","tags":"Etiquetas","tagsText":"Etiquetas Texto","multiIcon":"Icono multiselección","tabText":"Texto de la pestaña","tabAccent":"Acento de pestaña","checkedBackground":"Antecedentes comprobados","uncheckedBackground":"Fondo sin marcar","uncheckedBorder":"Frontera sin marcar","indicatorBackground":"Indicador Antecedentes","tableCellText":"Texto celular","selectedRowBackground":"Fondo de fila seleccionado","hoverRowBackground":"Fondo de la fila Hover","alternateRowBackground":"Fondo de fila alternativo","tableHeaderBackground":"Fondo de la cabecera","tableHeaderText":"Texto de cabecera","toolbarBackground":"Fondo de la barra de herramientas","toolbarText":"Texto de la barra de herramientas","pen":"Bolígrafo","footerIcon":"Icono de pie de página","tips":"Consejos","margin":"Margen","padding":"Acolchado","marginLeft":"Margen izquierdo","marginRight":"Margen derecho","marginTop":"Margen superior","marginBottom":"Margen inferior","containerheaderpadding":"Relleno de cabecera","containerfooterpadding":"Relleno de pie de página","containerbodypadding":"Acolchado corporal","minWidth":"Anchura mínima","aspectRatio":"Relación de aspecto","textSize":"Tamaño del texto"},"export":{"hiddenDesc":"Si es verdadero, el componente se oculta","disabledDesc":"Si es verdadero, el componente está desactivado y no es interactivo","visibleDesc":"Si es verdadero, el componente es visible","inputValueDesc":"Valor actual de la entrada","invalidDesc":"Indica si el valor no es válido","placeholderDesc":"Texto de marcador de posición cuando no se establece ningún valor","requiredDesc":"Si es verdadero, se requiere un valor válido","submitDesc":"Enviar formulario","richTextEditorValueDesc":"Valor actual del Editor","richTextEditorReadOnlyDesc":"Si es verdadero, el Editor es de sólo lectura","richTextEditorHideToolBarDesc":"Si es verdadero, la barra de herramientas se oculta","jsonEditorDesc":"Datos JSON actuales","sliderValueDesc":"Valor seleccionado actualmente","sliderMaxValueDesc":"Valor máximo del deslizador","sliderMinValueDesc":"Valor mínimo de la corredera","sliderStartDesc":"Valor del punto de partida seleccionado","sliderEndDesc":"Valor del punto final seleccionado","ratingValueDesc":"Clasificación seleccionada actualmente","ratingMaxDesc":"Valor nominal máximo","datePickerValueDesc":"Fecha seleccionada actualmente","datePickerFormattedValueDesc":"Fecha seleccionada formateada","datePickerTimestampDesc":"Marca de tiempo de la fecha seleccionada","dateRangeStartDesc":"Fecha de inicio del rango","dateRangeEndDesc":"Fecha final del intervalo","dateRangeStartTimestampDesc":"Marca de tiempo de la fecha de inicio","dateRangeEndTimestampDesc":"Marca de tiempo de la fecha de finalización","dateRangeFormattedValueDesc":"Rango de fechas formateado","dateRangeFormattedStartValueDesc":"Fecha de inicio formateada","dateRangeFormattedEndValueDesc":"Fecha final formateada","timePickerValueDesc":"Hora seleccionada actualmente","timePickerFormattedValueDesc":"Hora seleccionada formateada","timeRangeStartDesc":"Hora de inicio del alcance","timeRangeEndDesc":"Hora de finalización del alcance","timeRangeFormattedValueDesc":"Rango de tiempo formateado","timeRangeFormattedStartValueDesc":"Hora de inicio formateada","timeRangeFormattedEndValueDesc":"Hora final formateada"},"validationDesc":{"email":"Introduce una dirección de correo electrónico válida","url":"Por favor, introduce una URL válida","regex":"Concuerda con el patrón especificado","maxLength":"Demasiados caracteres, actual: {longitud}, máximo: {longitudmáxima}","minLength":"No hay suficientes caracteres, actual: {longitud}, mínimo: {longitud mínima}","maxValue":"El valor supera el máximo, actual: {valor}, máximo: {máximo}","minValue":"Valor por debajo del mínimo, actual: {valor}, mínimo: {mín}","maxTime":"El tiempo supera el máximo, actual: {hora}, máximo: {tiempomáx}","minTime":"Tiempo por debajo del mínimo, actual: {hora}, mínimo: {minTime}","maxDate":"La fecha supera el máximo, actual: {fecha}, máximo: {fechaMáx}","minDate":"Fecha por debajo del mínimo, actual: {fecha}, mínimo: {fechaMín}"},"query":{"noQueries":"No hay Consultas de Datos disponibles.","queryTutorialButton":"Ver documentos {valor}","datasource":"Tus fuentes de datos","newDatasource":"Nueva fuente de datos","generalTab":"General","notificationTab":"Notificación","advancedTab":"Avanzado","showFailNotification":"Mostrar notificación en caso de fallo","failCondition":"Condiciones de fallo","failConditionTooltip1":"Personaliza las condiciones de fallo y las notificaciones correspondientes.","failConditionTooltip2":"Si alguna condición devuelve verdadero, la consulta se marca como fallida y activa la notificación correspondiente.","showSuccessNotification":"Mostrar notificación de éxito","successMessageLabel":"Mensaje de éxito","successMessage":"Corre con éxito","notifyDuration":"Duración","notifyDurationTooltip":"Duración de la notificación. La unidad de tiempo puede ser \\'s\\' (segundo, por defecto) o \\'ms\\' (milisegundo). El valor por defecto es {default}s. El máximo es {max}s.","successMessageWithName":"{nombre} ejecutado correctamente","failMessageWithName":"Ha fallado la ejecución de {nombre}: {resultado}","showConfirmationModal":"Mostrar Modal de Confirmación Antes de Ejecutar","confirmationMessageLabel":"Mensaje de confirmación","confirmationMessage":"¿Estás seguro de que quieres ejecutar esta Consulta de Datos?","newQuery":"Nueva consulta de datos","newFolder":"Carpeta nueva","recentlyUsed":"Usado recientemente","folder":"Carpeta","folderNotEmpty":"La carpeta no está vacía","dataResponder":"Respondedor de datos","tempState":"Estado Temporal","transformer":"Transformador","quickRestAPI":"Consulta REST","quickStreamAPI":"Consulta de flujos","quickGraphql":"Consulta GraphQL","lowcoderAPI":"API Lowcoder","executeJSCode":"Ejecutar código JavaScript","importFromQueryLibrary":"Importar desde la biblioteca de consultas","importFromFile":"Importar desde archivo","triggerType":"Se activa cuando...","triggerTypeAuto":"Cambio de entradas o al cargar la página","triggerTypePageLoad":"Cuando se carga la Aplicación (Página)","triggerTypeManual":"Sólo cuando lo activas manualmente","chooseDataSource":"Elegir fuente de datos","method":"Método","updateExceptionDataSourceTitle":"Actualizar fuente de datos que falla","updateExceptionDataSourceContent":"Actualiza la siguiente consulta con la misma fuente de datos que falla:","update":"Actualiza","disablePreparedStatement":"Desactivar Declaraciones Preparadas","disablePreparedStatementTooltip":"Desactivar las sentencias preparadas puede generar SQL dinámico, pero aumenta el riesgo de inyección SQL","timeout":"Tiempo de espera tras","timeoutTooltip":"Unidad por defecto: ms. Unidades de entrada admitidas: ms, s. Valor por defecto: {defaultSeconds} segundos. Valor máximo: {maxSeconds} segundos. Por ejemplo, 300 (es decir, 300ms), 800ms, 5s.","periodic":"Ejecuta periódicamente esta consulta de datos","periodicTime":"Periodo","periodicTimeTooltip":"Periodo entre ejecuciones sucesivas. Unidad por defecto: ms. Unidades de entrada admitidas: ms, s. Valor mínimo: 100ms. La ejecución periódica se desactiva para valores inferiores a 100ms. Por ejemplo, 300 (es decir, 300ms), 800ms, 5s.","cancelPrevious":"Ignorar los resultados de ejecuciones anteriores no completadas","cancelPreviousTooltip":"Si se desencadena una nueva ejecución, se ignorará el resultado de las ejecuciones anteriores no completadas si no se completaron, y estas ejecuciones ignoradas no desencadenarán la lista de eventos de la consulta.","dataSourceStatusError":"Si se desencadena una nueva ejecución, se ignorará el resultado de las ejecuciones anteriores no completadas, y las ejecuciones ignoradas no desencadenarán la lista de eventos de la consulta.","success":"Éxito","fail":"Fallo","successDesc":"Se activa cuando la ejecución tiene éxito","failDesc":"Se activa cuando falla la ejecución","fixedDelayError":"Consulta no ejecutada","execSuccess":"Corre con éxito","execFail":"Error de ejecución","execIgnored":"Los resultados de esta consulta fueron ignorados","deleteSuccessMessage":"Eliminado con éxito. Puedes utilizar {undoKey} para Deshacer","dataExportDesc":"Datos obtenidos por la consulta actual","codeExportDesc":"Código de estado de la consulta actual","successExportDesc":"Si la consulta actual se ha ejecutado correctamente","messageExportDesc":"Información devuelta por la consulta actual","extraExportDesc":"Otros datos de la consulta actual","isFetchingExportDesc":"¿Es la consulta actual de la petición?","runTimeExportDesc":"Tiempo de ejecución de la consulta actual (ms)","latestEndTimeExportDesc":"Último tiempo de ejecución","triggerTypeExportDesc":"Tipo de gatillo","chooseResource":"Elige un Recurso","createDataSource":"Crear una nueva fuente de datos","editDataSource":"Edita","datasourceName":"Nombre","datasourceNameRuleMessage":"Introduce un nombre de fuente de datos","generalSetting":"Configuración general","advancedSetting":"Ajustes avanzados","port":"Puerto","portRequiredMessage":"Introduce un puerto","portErrorMessage":"Introduce un puerto correcto","connectionType":"Tipo de conexión","regular":"Regular","host":"Anfitrión","hostRequiredMessage":"Introduce un nombre de dominio de host o una dirección IP","userName":"Nombre de usuario","password":"Contraseña","encryptedServer":"-------- Cifrado en el lado del servidor --------","uriRequiredMessage":"Introduce un URI","urlRequiredMessage":"Introduce una URL","uriErrorMessage":"Por favor, introduce un URI correcto","urlErrorMessage":"Introduce una URL correcta","httpRequiredMessage":"Introduce http:// o https://","databaseName":"Nombre de la base de datos","databaseNameRequiredMessage":"Introduce el nombre de la base de datos","useSSL":"Utiliza SSL","userNameRequiredMessage":"Introduce tu nombre","passwordRequiredMessage":"Introduce tu contraseña","authentication":"Autenticación","authenticationType":"Tipo de autenticación","sslCertVerificationType":"Verificación de certificados SSL","sslCertVerificationTypeDefault":"Verificar CA Cert","sslCertVerificationTypeSelf":"Verificar certificado autofirmado","sslCertVerificationTypeDisabled":"Discapacitados","selfSignedCert":"Certificado autofirmado","selfSignedCertRequireMsg":"Introduce tu certificado","enableTurnOffPreparedStatement":"Activar la alternancia de sentencias preparadas para consultas","enableTurnOffPreparedStatementTooltip":"Puedes activar o desactivar las sentencias preparadas en la pestaña Avanzado de la consulta","serviceName":"Nombre del servicio","serviceNameRequiredMessage":"Introduce el nombre de tu servicio","useSID":"Utiliza el SID","connectSuccessfully":"Conexión correcta","saveSuccessfully":"Guardado correctamente","database":"Base de datos","cloudHosting":"Lowcoder alojado en la nube no puede acceder a los servicios locales utilizando 127.0.0.1 o localhost. Intenta conectarte a fuentes de datos de la red pública o utiliza un proxy inverso para los servicios privados.","notCloudHosting":"Para el despliegue alojado en Docker, Lowcoder utiliza redes puente, por lo que 127.0.0.1 y localhost no son válidos para las direcciones de host. Para acceder a las fuentes de datos de la máquina local, consulta","howToAccessHostDocLink":"Cómo acceder a la API/DB del host","returnList":"Devuelve","chooseDatasourceType":"Elige el tipo de fuente de datos","viewDocuments":"Ver documentos","testConnection":"Conexión de prueba","save":"Guarda","whitelist":"Lista de permisos","whitelistTooltip":"Añade las direcciones IP de Lowcoder\\ a tu lista de fuentes de datos permitidas según sea necesario.","address":"Dirección: ","nameExists":"El nombre {nombre} ya existe","jsQueryDocLink":"Acerca de la consulta JavaScript","dynamicDataSourceConfigLoadingText":"Cargando configuración extra de fuente de datos...","dynamicDataSourceConfigErrText":"No se ha podido cargar la configuración adicional de la fuente de datos.","retry":"Reintentar"},"sqlQuery":{"keyValuePairs":"Pares clave-valor","object":"Objeto","allowMultiModify":"Permitir la modificación de varias filas","allowMultiModifyTooltip":"Si se selecciona, se operan todas las filas que cumplan las condiciones. En caso contrario, sólo se operará sobre la primera fila que cumpla las condiciones.","array":"Matriz","insertList":"Insertar lista","insertListTooltip":"Valores insertados cuando no existen","filterRule":"Regla de filtrado","updateList":"Actualizar lista","updateListTooltip":"Los valores actualizados tal como existen pueden ser anulados por los mismos valores de la lista de inserción","sqlMode":"Modo SQL","guiMode":"Modo GUI","operation":"Operación","insert":"Inserta","upsert":"Insertar, pero Actualizar si hay conflicto","update":"Actualiza","delete":"Borra","bulkInsert":"Inserción a granel","bulkUpdate":"Actualización masiva","table":"Tabla","primaryKeyColumn":"Columna de clave primaria"},"EsQuery":{"rawCommand":"Comando en bruto","queryTutorialButton":"Ver documentos de la API de Elasticsearch","request":"Solicita"},"googleSheets":{"rowIndex":"Índice de filas","spreadsheetId":"ID de la hoja de cálculo","sheetName":"Nombre de la hoja","readData":"Leer datos","appendData":"Añadir fila","updateData":"Actualizar Fila","deleteData":"Borrar fila","clearData":"Fila clara","serviceAccountRequireMessage":"Introduce tu cuenta de servicio","ASC":"ASC","DESC":"DESC","sort":"Clasificar","sortPlaceholder":"Nombre"},"queryLibrary":{"export":"Exportar a JSON","noInput":"La consulta actual no tiene entrada","inputName":"Nombre","inputDesc":"Descripción","emptyInputs":"Sin entradas","clickToAdd":"Añade","chooseQuery":"Elige Consulta","viewQuery":"Ver consulta","chooseVersion":"Elegir versión","latest":"Lo último en","publish":"Publica","historyVersion":"Historia Versión","deleteQueryLabel":"Borrar consulta","deleteQueryContent":"No se puede recuperar la consulta después de borrarla. ¿Borrar la consulta?","run":"Ejecuta","readOnly":"Sólo lectura","exit":"Salir","recoverAppSnapshotContent":"Restaurar la consulta actual a la versión {versión}","searchPlaceholder":"Consulta de búsqueda","allQuery":"Todas las consultas","deleteQueryTitle":"Borrar consulta","unnamed":"Sin nombre","publishNewVersion":"Publicar nueva versión","publishSuccess":"Publicado con éxito","version":"Versión","desc":"Descripción"},"snowflake":{"accountIdentifierTooltip":"Consulta ","extParamsTooltip":"Configurar parámetros de conexión adicionales"},"lowcoderQuery":{"queryOrgUsers":"Consultar usuarios del espacio de trabajo"},"redisQuery":{"rawCommand":"Comando en bruto","command":"Mando","queryTutorial":"Ver documentos sobre los comandos de Redis"},"httpQuery":{"bodyFormDataTooltip":"Si se selecciona {tipo}, el formato del valor debe ser {objeto}. Ejemplo: {ejemplo}","text":"Texto","file":"Archivo","extraBodyTooltip":"Los valores clave del cuerpo extra se añadirán al cuerpo con tipos de datos JSON o de formulario","forwardCookies":"Adelante Cookies","forwardAllCookies":"Reenviar todas las cookies"},"smtpQuery":{"attachment":"Adjunto","attachmentTooltip":"Puede utilizarse con el componente de carga de archivos, los datos deben convertirse a: ","MIMETypeUrl":"https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types","sender":"Remitente","recipient":"Destinatario","carbonCopy":"Copia al carbón","blindCarbonCopy":"Copia al carbón ciega","subject":"Asunto","content":"Contenido","contentTooltip":"Admite la introducción de texto o HTML"},"uiCompCategory":{"dashboards":"Cuadros de mando e informes","layout":"Diseño y navegación","forms":"Recogida de datos y formularios","collaboration":"Reunión y colaboración","projectmanagement":"Gestión de proyectos","scheduling":"Calendario y programación","documents":"Gestión de documentos y archivos","itemHandling":"Tramitación de artículos y firmas","multimedia":"Multimedia y animación","integration":"Integración y ampliación"},"uiComp":{"autoCompleteCompName":"Auto Completo","autoCompleteCompDesc":"Un campo de entrada que proporciona sugerencias mientras escribes, mejorando la experiencia del usuario y la precisión.","autoCompleteCompKeywords":"sugerencias, autocompletar, escribir, entrada","inputCompName":"Entrada","inputCompDesc":"Un campo de entrada de texto básico que permite a los usuarios introducir y editar texto.","inputCompKeywords":"texto, entrada, campo, editar","textAreaCompName":"Área de texto","textAreaCompDesc":"Una entrada de texto de varias líneas para contenidos de forma más larga, como comentarios o descripciones.","textAreaCompKeywords":"multilínea, área de texto, entrada, texto","passwordCompName":"Contraseña","passwordCompDesc":"Un campo seguro para introducir la contraseña, enmascarando los caracteres para mayor privacidad.","passwordCompKeywords":"contraseña, seguridad, entrada, oculto","richTextEditorCompName":"Editor de texto enriquecido","richTextEditorCompDesc":"Un editor de texto avanzado que admite numerosas opciones de formato, como negrita, cursiva y listas.","richTextEditorCompKeywords":"editor, texto, formato, contenido enriquecido","numberInputCompName":"Número Entrada","numberInputCompDesc":"Un campo específico para la introducción de datos numéricos, con controles para aumentar y disminuir los valores.","numberInputCompKeywords":"número, entrada, incremento, decremento","sliderCompName":"Deslizador","sliderCompDesc":"Componente gráfico deslizante para seleccionar un valor o rango dentro de una escala definida.","sliderCompKeywords":"deslizador, rango, entrada, gráfico","rangeSliderCompName":"Corredera de alcance","rangeSliderCompDesc":"Un deslizador de doble asa para seleccionar un rango de valores, útil para filtrar o establecer límites.","rangeSliderCompKeywords":"gama, deslizador, doble asa, filtro","ratingCompName":"Clasificación","ratingCompDesc":"Un componente para capturar las valoraciones de los usuarios, mostradas como estrellas.","ratingCompKeywords":"valoración, estrellas, opiniones, aportaciones","switchCompName":"Interruptor","switchCompDesc":"Un interruptor basculante para decisiones del tipo encendido/apagado o sí/no.","switchCompKeywords":"conmutador, interruptor, encendido/apagado, control","selectCompName":"Selecciona","selectCompDesc":"Un menú desplegable para seleccionar entre una lista de opciones.","selectCompKeywords":"desplegable, seleccionar, opciones, menú","multiSelectCompName":"Multiselector","multiSelectCompDesc":"Un componente que permite seleccionar varios elementos de una lista desplegable.","multiSelectCompKeywords":"multiselección, múltiple, desplegable, opciones","cascaderCompName":"Cascader","cascaderCompDesc":"Un desplegable de varios niveles para la selección jerárquica de datos, como la selección de una ubicación.","cascaderCompKeywords":"cascada, jerárquico, desplegable, niveles","checkboxCompName":"Casilla de verificación","checkboxCompDesc":"Una casilla de verificación estándar para opciones que se pueden seleccionar o deseleccionar.","checkboxCompKeywords":"casilla, opciones, seleccionar, alternar","radioCompName":"Radio","radioCompDesc":"Botones de radio para seleccionar una opción de un conjunto, donde sólo se permite una elección.","radioCompKeywords":"radio, botones, seleccionar, elección única","segmentedControlCompName":"Control segmentado","segmentedControlCompDesc":"Un control con opciones segmentadas para alternar rápidamente entre varias opciones.","segmentedControlCompKeywords":"segmentado, control, conmutar, opciones","fileUploadCompName":"Carga de archivos","fileUploadCompDesc":"Un componente para subir archivos, con soporte para arrastrar y soltar y selección de archivos.","fileUploadCompKeywords":"archivo, subir, arrastrar y soltar, seleccionar","dateCompName":"Fecha","dateCompDesc":"Un componente selector de fechas para seleccionar fechas de una interfaz de calendario.","dateCompKeywords":"fecha, selector, calendario, seleccionar","dateRangeCompName":"Rango de fechas","dateRangeCompDesc":"Un componente para seleccionar un intervalo de fechas, útil para sistemas de reservas o filtros.","dateRangeCompKeywords":"daterange, seleccionar, reservar, filtrar","timeCompName":"Tiempo","timeCompDesc":"Un componente de selección de hora para elegir horas concretas del día.","timeCompKeywords":"hora, selector, seleccionar, reloj","timeRangeCompName":"Rango temporal","timeRangeCompDesc":"Componente para seleccionar un intervalo de tiempo, utilizado a menudo en aplicaciones de programación.","timeRangeCompKeywords":"timerange, seleccionar, programación, duración","buttonCompName":"Botón Formulario","buttonCompDesc":"Un componente de botón versátil para enviar formularios, desencadenar acciones o navegar.","buttonCompKeywords":"botón, enviar, acción, navegar","linkCompName":"Enlace","linkCompDesc":"Un componente de visualización de hipervínculos para navegar o enlazar con recursos externos.","linkCompKeywords":"enlace, hipervínculo, navegación, externo","scannerCompName":"Escáner","scannerCompDesc":"Un componente para escanear códigos de barras, códigos QR y otros datos similares.","scannerCompKeywords":"escáner, código de barras, código QR, escanear","dropdownCompName":"Desplegable","dropdownCompDesc":"Un menú desplegable para mostrar de forma compacta una lista de opciones.","dropdownCompKeywords":"desplegable, menú, opciones, seleccionar","toggleButtonCompName":"Botón Alternar","toggleButtonCompDesc":"Un botón que puede alternar entre dos estados u opciones.","toggleButtonCompKeywords":"conmutar, botón, interruptor, estado","textCompName":"Visualización de texto","textCompDesc":"Un componente sencillo para mostrar contenido de texto estático o dinámico con formato Markdown incluido.","textCompKeywords":"texto, visualización, estático, dinámico","tableCompName":"Tabla","tableCompDesc":"Un componente de tabla enriquecido para mostrar datos en formato de tabla estructurada, con opciones de ordenación y filtrado, visualización de Datos en árbol y Filas extensibles.","tableCompKeywords":"tabla, datos, ordenar, filtrar","imageCompName":"Imagen","imageCompDesc":"Un componente para mostrar imágenes, compatible con varios formatos basados en datos URI o Base64.","imageCompKeywords":"imagen, visualización, medios, Base64","progressCompName":"Progreso","progressCompDesc":"Un indicador visual de progreso, utilizado normalmente para mostrar el estado de finalización de una tarea.","progressCompKeywords":"progreso, indicador, estado, tarea","progressCircleCompName":"Círculo de Progreso","progressCircleCompDesc":"Un indicador de progreso circular, utilizado a menudo para estados de carga o tareas con límite de tiempo.","progressCircleCompKeywords":"círculo, progreso, indicador, carga","fileViewerCompName":"Visor de archivos","fileViewerCompDesc":"Un componente para visualizar varios tipos de archivos, incluidos documentos e imágenes.","fileViewerCompKeywords":"archivo, visor, documento, imagen","dividerCompName":"Divisor","dividerCompDesc":"Componente divisor visual, utilizado para separar contenidos o secciones en un diseño.","dividerCompKeywords":"divisor, separador, disposición, diseño","qrCodeCompName":"Código QR","qrCodeCompDesc":"Un componente para mostrar códigos QR, útil para escanearlos rápidamente y transferir información.","qrCodeCompKeywords":"Código QR, escaneo, código de barras, información","formCompName":"Formulario","formCompDesc":"Un componente contenedor para construir formularios estructurados con varios tipos de entrada.","formCompKeywords":"formulario, entrada, contenedor, estructura","jsonSchemaFormCompName":"Formulario de esquema JSON","jsonSchemaFormCompDesc":"Un componente de formulario dinámico generado a partir de un esquema JSON.","jsonSchemaFormCompKeywords":"JSON, esquema, formulario, dinámico","containerCompName":"Contenedor","containerCompDesc":"Un contenedor de uso general para el diseño y la organización de los elementos de la interfaz de usuario.","containerCompKeywords":"contenedor, diseño, organización, IU","collapsibleContainerCompName":"Contenedor plegable","collapsibleContainerCompDesc":"Un contenedor que puede expandirse o colapsarse, ideal para gestionar la visibilidad del contenido.","collapsibleContainerCompKeywords":"plegable, contenedor, expandir, colapsar","tabbedContainerCompName":"Contenedor con pestañas","tabbedContainerCompDesc":"Un contenedor con navegación por pestañas para organizar el contenido en paneles separados.","tabbedContainerCompKeywords":"pestañas, contenedor, navegación, paneles","modalCompName":"Modal","modalCompDesc":"Un componente modal emergente para mostrar contenido, alertas o formularios en foco.","modalCompKeywords":"modal, popup, alerta, formulario","listViewCompName":"Ver lista","listViewCompDesc":"Un componente para mostrar una lista de elementos o datos, en cuyo interior puedes colocar otros componentes. Como un repetidor.","listViewCompKeywords":"lista, vista, visualización, repetidor","gridCompName":"Rejilla","gridCompDesc":"Un componente de cuadrícula flexible para crear diseños estructurados con filas y columnas como extensión del componente Vista Lista.","gridCompKeywords":"cuadrícula, diseño, filas, columnas","navigationCompName":"Navegación","navigationCompDesc":"Un componente de navegación para crear menús, migas de pan o pestañas para la navegación por el sitio.","navigationCompKeywords":"navegación, menú, migas de pan, pestañas","iframeCompName":"IFrame","iframeCompDesc":"Un componente de marco en línea para incrustar páginas web externas y aplicaciones o contenidos dentro de la aplicación.","iframeCompKeywords":"iframe, incrustar, página web, contenido","customCompName":"Componente personalizado","customCompDesc":"Un componente flexible y programable para crear elementos de interfaz de usuario únicos, definidos por el usuario y adaptados a tus necesidades específicas.","customCompKeywords":"personalizado, definido por el usuario, flexible, programable","moduleCompName":"Módulo","moduleCompDesc":"Utiliza Módulos para crear Micro-Apps diseñadas para encapsular funcionalidades o características específicas. Los módulos pueden incrustarse y reutilizarse en todas las aplicaciones.","moduleCompKeywords":"módulo, micro-app, funcionalidad, reutilizable","jsonExplorerCompName":"Explorador JSON","jsonExplorerCompDesc":"Un componente para explorar visualmente e interactuar con estructuras de datos JSON.","jsonExplorerCompKeywords":"JSON, explorador, datos, estructura","jsonEditorCompName":"Editor JSON","jsonEditorCompDesc":"Un componente editor para crear y modificar datos JSON con validación y resaltado de sintaxis.","jsonEditorCompKeywords":"JSON, editor, modificar, validar","treeCompName":"Árbol","treeCompDesc":"Un componente de estructura de árbol para mostrar datos jerárquicos, como sistemas de archivos u organigramas.","treeCompKeywords":"árbol, jerárquico, datos, estructura","treeSelectCompName":"Seleccionar árbol","treeSelectCompDesc":"Un componente de selección que presenta las opciones en formato de árbol jerárquico, permitiendo selecciones organizadas y anidadas.","treeSelectCompKeywords":"árbol, seleccionar, jerárquico, anidado","audioCompName":"Audio","audioCompDesc":"Un componente para incrustar contenido de audio, con controles para la reproducción y el ajuste del volumen.","audioCompKeywords":"audio, reproducción, sonido, música","videoCompName":"Vídeo","videoCompDesc":"Un componente multimedia para incrustar y reproducir contenidos de vídeo, compatible con varios formatos.","videoCompKeywords":"vídeo, multimedia, reproducción, incrustar","drawerCompName":"Cajón","drawerCompDesc":"Componente de un panel deslizante que puede utilizarse para navegación adicional o visualización de contenidos, y que suele emerger del borde de la pantalla.","drawerCompKeywords":"cajón, corredera, panel, navegación","chartCompName":"Gráfico","chartCompDesc":"Un componente versátil para visualizar datos mediante diversos tipos de tablas y gráficos.","chartCompKeywords":"tabla, gráfico, datos, visualización","carouselCompName":"Carrusel de imágenes","carouselCompDesc":"Un componente de carrusel giratorio para mostrar imágenes, banners o diapositivas de contenido.","carouselCompKeywords":"carrusel, imágenes, rotación, escaparate","imageEditorCompName":"Editor de imágenes","imageEditorCompDesc":"Un componente interactivo para editar y manipular imágenes, que ofrece diversas herramientas y filtros.","imageEditorCompKeywords":"imagen, editor, manipular, herramientas","mermaidCompName":"Cartas de sirenas","mermaidCompDesc":"Un componente para representar diagramas y organigramas complejos basados en la sintaxis Mermaid.","mermaidCompKeywords":"sirena, gráficos, diagramas, organigramas","calendarCompName":"Calendario","calendarCompDesc":"Un componente de calendario para mostrar fechas y eventos, con opciones de vistas de mes, semana o día.","calendarCompKeywords":"calendario, fechas, eventos, programación","signatureCompName":"Firma","signatureCompDesc":"Un componente para capturar firmas digitales, útil para procesos de aprobación y verificación.","signatureCompKeywords":"firma, digital, aprobación, verificación","jsonLottieCompName":"Animación Lottie","jsonLottieCompDesc":"Un componente para mostrar animaciones Lottie, que proporciona animaciones ligeras y escalables basadas en datos JSON.","jsonLottieCompKeywords":"lottie, animación, JSON, escalable","timelineCompName":"Cronología","timelineCompDesc":"Componente para mostrar acontecimientos o acciones en orden cronológico, representados visualmente a lo largo de una línea de tiempo lineal.","timelineCompKeywords":"cronología, acontecimientos, cronológico, historia","commentCompName":"Comentario","commentCompDesc":"Un componente para añadir y mostrar comentarios de los usuarios, que admite respuestas en hilos y la interacción de los usuarios.","commentCompKeywords":"comentario, debate, interacción con el usuario, respuesta","mentionCompName":"Menciona","mentionCompDesc":"Componente que permite mencionar usuarios o etiquetas dentro de un contenido de texto, utilizado normalmente en redes sociales o plataformas colaborativas.","mentionCompKeywords":"mencionar, etiquetar, usuario, redes sociales","responsiveLayoutCompName":"Diseño adaptable","responsiveLayoutCompDesc":"Un componente de diseño diseñado para adaptarse y responder a diferentes tamaños de pantalla y dispositivos, garantizando una experiencia de usuario coherente.","responsiveLayoutCompKeywords":"responsive, diseño, adaptar, tamaño pantalla"},"comp":{"menuViewDocs":"Ver documentación","menuViewPlayground":"Ver zona de juegos interactiva","menuUpgradeToLatest":"Actualizar a la última versión","nameNotEmpty":"No puede estar vacío","nameRegex":"Debe empezar por una letra y contener sólo letras, cifras y guiones bajos (_)","nameJSKeyword":"No puede ser una palabra clave JavaScript","nameGlobalVariable":"No puede ser un nombre de variable global","nameExists":"El nombre {nombre} ya existe","getLatestVersionMetaError":"No se ha podido obtener la última versión, inténtalo más tarde.","needNotUpgrade":"La versión actual ya es la última.","compNotFoundInLatestVersion":"Componente actual no encontrado en la última versión.","upgradeSuccess":"Actualizado correctamente a la última versión.","searchProp":"Busca en"},"jsonSchemaForm":{"retry":"Reintentar","resetAfterSubmit":"Restablecer después de enviar correctamente el formulario","jsonSchema":"Esquema JSON","uiSchema":"Esquema de IU","schemaTooltip":"Consulta","defaultData":"Datos de formulario precargados","dataDesc":"Datos del formulario actual","required":"Necesario","maximum":"El valor máximo es {valor}","minimum":"El valor mínimo es {valor}","exclusiveMaximum":"Debe ser inferior a {valor}","exclusiveMinimum":"Debe ser mayor que {valor}","multipleOf":"Debe ser múltiplo de {valor}","minLength":"Al menos {valor} Caracteres","maxLength":"Como máximo {valor} Caracteres","pattern":"Debe coincidir con el patrón {valor}","format":"Debe coincidir con el formato {valor}"},"select":{"inputValueDesc":"Valor de búsqueda de entrada"},"customComp":{"text":"Es un buen día.","triggerQuery":"Consulta desencadenante","updateData":"Actualizar datos","updateText":"¡También estoy de buen humor para desarrollar ahora mi propio componente personalizado con Lowcoder!","sdkGlobalVarName":"Lowcoder","data":"Datos que quieres pasar al Componente personalizado","code":"Código de tu componente personalizado"},"tree":{"selectType":"Selecciona el tipo","noSelect":"No Seleccionar","singleSelect":"Selección única","multiSelect":"Selección múltiple","checkbox":"Casilla de verificación","checkedStrategy":"Estrategia comprobada","showAll":"Todos los nodos","showParent":"Sólo nodos padre","showChild":"Nodos hijos únicos","autoExpandParent":"Auto Expandir Padre","checkStrictly":"Comprobar estrictamente","checkStrictlyTooltip":"Comprueba con precisión el Nodo del Árbol; el Nodo del Árbol padre y los Nodos del Árbol hijos no están asociados","treeData":"Datos del árbol","treeDataDesc":"Datos actuales del árbol","value":"Valores por defecto","valueDesc":"Valores actuales","expanded":"Valores ampliados","expandedDesc":"Valores ampliados actuales","defaultExpandAll":"Por defecto Expandir todos los nodos","showLine":"Mostrar línea","showLeafIcon":"Mostrar icono de hoja","treeDataAsia":"Asia","treeDataChina":"China","treeDataBeijing":"Pekín","treeDataShanghai":"Shanghai","treeDataJapan":"Japón","treeDataEurope":"Europa","treeDataEngland":"Inglaterra","treeDataFrance":"Francia","treeDataGermany":"Alemania","treeDataNorthAmerica":"América del Norte","helpLabel":"Etiqueta de nodo","helpValue":"Valor único del nodo en el árbol","helpChildren":"Niños Nodos","helpDisabled":"Desactiva el Nodo","helpSelectable":"Si el Nodo es Seleccionable (Tipo de Selección Simple/Múltiple)","helpCheckable":"Si mostrar casilla de verificación (Tipo de casilla de verificación)","helpDisableCheckbox":"Desactiva la casilla de verificación (Tipo de casilla de verificación)"},"moduleContainer":{"eventTest":"Prueba de Evento","methodTest":"Método de ensayo","inputTest":"Prueba de entrada"},"password":{"label":"Contraseña","visibilityToggle":"Conmutar visibilidad"},"richTextEditor":{"toolbar":"Personalizar la barra de herramientas","toolbarDescription":"Puedes personalizar la barra de herramientas. Consulta: https://quilljs.com/docs/modules/toolbar/ para más detalles.","placeholder":"Por favor, introduce...","hideToolbar":"Ocultar barra de herramientas","content":"Contenido","title":"Título","save":"Guarda","link":"Enlace: ","edit":"Edita","remove":"Elimina","defaultValue":"Contenido básico"},"numberInput":{"formatter":"Formato","precision":"Precisión","allowNull":"Permitir valor nulo","thousandsSeparator":"Mostrar separador de miles","controls":"Mostrar botones de aumento/disminución","step":"Paso","standard":"Estándar","percent":"Porcentaje"},"slider":{"step":"Paso","stepTooltip":"El valor debe ser mayor que 0 y divisible por (Máx-Mín)"},"rating":{"max":"Clasificación máxima","allowHalf":"Permitir la mitad de puntos de valoración"},"optionsControl":{"optionList":"Opciones","option":"Opción","optionI":"Opción {i}","viewDocs":"Ver documentos","tip":"Las variables \\'item\\' y \\'i\\' representan el valor y el índice de cada elemento de la matriz de datos"},"radio":{"options":"Opciones","horizontal":"Horizontal","horizontalTooltip":"La Disposición Horizontal Se Enrolla Cuando Se Queda Sin Espacio","vertical":"Vertical","verticalTooltip":"El diseño vertical siempre se mostrará en una sola columna","autoColumns":"Columna Auto","autoColumnsTooltip":"La disposición en autocolumnas reordena automáticamente el orden según lo permita el espacio y se muestra como varias columnas"},"cascader":{"options":"Datos JSON para mostrar selecciones en cascada"},"selectInput":{"valueDesc":"Valor seleccionado actualmente","selectedIndexDesc":"El índice del valor seleccionado actualmente, o -1 si no hay ningún valor seleccionado","selectedLabelDesc":"La etiqueta del valor seleccionado actualmente"},"file":{"typeErrorMsg":"Debe ser un número con una unidad de tamaño de archivo válida, o un número de bytes sin unidad.","fileEmptyErrorMsg":"Carga fallida. El tamaño del archivo está vacío.","fileSizeExceedErrorMsg":"Carga fallida. El tamaño del archivo supera el límite.","minSize":"Tamaño mínimo","minSizeTooltip":"El Tamaño Mínimo de los Archivos Subidos con Unidades de Tamaño de Archivo Opcionales (por ejemplo, \\'5kb\\', \\'10 MB\\'). Si no se proporciona ninguna unidad, el valor se considerará un número de bytes.","maxSize":"Tamaño máximo","maxSizeTooltip":"El tamaño máximo de los archivos subidos con unidades opcionales de tamaño de archivo (por ejemplo, \\'5kb\\', \\'10 MB\\'). Si no se proporciona ninguna unidad, el valor se considerará un número de bytes.","single":"Individual","multiple":"Múltiple","directory":"Directorio","upload":"Navega por","fileType":"Tipos de archivos","reference":"Consulta","fileTypeTooltipUrl":"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers","fileTypeTooltip":"Especificadores únicos de tipo de archivo","uploadType":"Tipo de carga","showUploadList":"Mostrar lista de cargas","maxFiles":"Archivos Max","filesValueDesc":"El contenido del archivo cargado actualmente está codificado en Base64","filesDesc":"Lista de los archivos cargados actualmente. Para más detalles, consulta","clearValueDesc":"Borrar todos los archivos","parseFiles":"Analizar archivos","parsedValueTooltip1":"Si parseFiles es True, los archivos cargados se convertirán en objetos, matrices o cadenas. Se puede acceder a los datos analizados a través de la matriz parsedValue.","parsedValueTooltip2":"Admite archivos Excel, JSON, CSV y de texto. Otros formatos devolverán un valor nulo."},"date":{"format":"Formato","formatTip":"Admite: \\AAAA-MM-DD HH:mm:ss\", \"AAAA-MM-DD\", \"Timestamp\".","reference":"Consulta","showTime":"Hora del espectáculo","start":"Fecha de inicio","end":"Fecha final","year":"Año","quarter":"Cuarto","month":"Mes","week":"Semana","date":"Fecha","clearAllDesc":"Borrar todo","resetAllDesc":"Restablecer todo","placeholder":"Seleccionar fecha","placeholderText":"Marcador de posición","startDate":"Fecha de inicio","endDate":"Fecha final"},"time":{"start":"Hora de inicio","end":"Fin de los tiempos","formatTip":"Soporte: \\'HH:mm:ss\\', \\'Timestamp\\'","format":"Formato","placeholder":"Selecciona Hora","placeholderText":"Marcador de posición","startTime":"Hora de inicio","endTime":"Fin de los tiempos"},"button":{"prefixIcon":"Icono Prefijo","suffixIcon":"Icono Sufijo","icon":"Icono","iconSize":"Tamaño del icono","button":"Botón Formulario","formToSubmit":"Formulario para enviar","default":"Por defecto","submit":"Envía","textDesc":"Texto mostrado actualmente en el botón","loadingDesc":"¿Está el Botón en Estado de Carga? Si es cierto, el botón actual está cargando","formButtonEvent":"Evento"},"link":{"link":"Enlace","textDesc":"Texto mostrado actualmente en el enlace","loadingDesc":"¿Está el Enlace en Estado de Carga? Si es Verdadero, el Enlace Actual Está Cargando"},"scanner":{"text":"Haz clic en Escanear","camera":"Cámara {índice}","changeCamera":"Cambiar cámara","continuous":"Escaneado continuo","uniqueData":"Ignorar datos duplicados","maskClosable":"Pulsa la Máscara para Cerrar","errTip":"Utiliza este componente bajo HTTPS o Localhost"},"dropdown":{"onlyMenu":"Pantalla sólo con etiqueta","textDesc":"Texto mostrado actualmente en el botón"},"textShow":{"text":"### 👋 Hola, {nombre}","valueTooltip":"Markdown admite la mayoría de etiquetas y atributos HTML. iframe, Script y otras etiquetas están desactivadas por motivos de seguridad.","verticalAlignment":"Alineación vertical","horizontalAlignment":"Alineación horizontal","textDesc":"Texto mostrado en el cuadro de texto actual"},"table":{"editable":"Editable","columnNum":"Columnas","viewModeResizable":"Ancho de columna ajustado por el usuario","viewModeResizableTooltip":"Si los usuarios pueden ajustar el ancho de columna.","showFilter":"Botón Mostrar filtro","showRefresh":"Mostrar botón Actualizar","showDownload":"Mostrar botón de descarga","columnSetting":"Botón Mostrar ajuste de columna","searchText":"Buscar texto","searchTextTooltip":"Buscar y Filtrar los Datos Presentados en la Tabla","showQuickJumper":"Mostrar saltador rápido","hideOnSinglePage":"Ocultar en una sola página","showSizeChanger":"Mostrar botón de cambio de tamaño","pageSizeOptions":"Opciones de tamaño de página","pageSize":"Tamaño de página","total":"Recuento total de filas","totalTooltip":"El valor por defecto es el número de elementos de datos actuales, que se pueden obtener de la consulta, por ejemplo: \\'{{query1.data[0].count}}\\'","filter":"Filtrar","filterRule":"Regla de filtrado","chooseColumnName":"Elegir columna","chooseCondition":"Elegir condición","clear":"Claro","columnShows":"Columnas","selectAll":"Seleccionar todo","and":"Y","or":"O","contains":"Contiene","notContain":"No contiene","equals":"Es igual a","isNotEqual":"No es igual","isEmpty":"Está vacío","isNotEmpty":"No está vacío","greater":"Mayor que","greaterThanOrEquals":"Mayor o igual que","lessThan":"Menos de","lessThanOrEquals":"Menor o igual que","action":"Acción","columnValue":"Valor de columna","columnValueTooltip":"\\'{{currentCell}}\\': Datos celulares actuales{{currentRow}}\\': Datos de la fila actual{{currentIndex}}\\': Índice de Datos Actuales (Empezando por 0)\\n Ejemplo: \\'{{CeldaActual * 5}}' Mostrar 5 Veces el Valor Original de los Datos.","imageSrc":"Fuente de la imagen","imageSize":"Tamaño de la imagen","columnTitle":"Título","sortable":"Clasificable","align":"Alineación","fixedColumn":"Columna fija","autoWidth":"Ancho automático","customColumn":"Columna personalizada","auto":"Auto","fixed":"Fijo","columnType":"Tipo de columna","float":"Flotador","prefix":"Prefijo","suffix":"Sufijo","text":"Texto","number":"Número","link":"Enlace","links":"Enlaces","tag":"Etiqueta","date":"Fecha","dateTime":"Fecha Hora","badgeStatus":"Estado","button":"Botón","image":"Imagen","boolean":"Booleano","rating":"Clasificación","progress":"Progreso","option":"Operación","optionList":"Lista de operaciones","option1":"Operación 1","status":"Estado","statusTooltip":"Valores opcionales: Correcto, Error, Predeterminado, Advertencia, Procesando","primaryButton":"Primaria","defaultButton":"Por defecto","type":"Tipo","tableSize":"Tamaño de la tabla","hideHeader":"Ocultar cabecera de tabla","fixedHeader":"Cabecera de tabla fija","fixedHeaderTooltip":"La cabecera será fija para la tabla desplazable verticalmente","fixedToolbar":"Barra de herramientas fija","fixedToolbarTooltip":"La barra de herramientas se fijará para la tabla desplazable verticalmente en función de la posición","hideBordered":"Ocultar borde de columna","deleteColumn":"Borrar columna","confirmDeleteColumn":"Confirmar Borrar columna: ","small":"S","middle":"M","large":"L","refreshButtonTooltip":"Los Datos Actuales Cambian, Pulsa para Regenerar la Columna.","changeSetDesc":"Un Objeto que Representa Cambios en una Tabla Editable, Sólo Contiene la Celda Cambiada. Las filas van primero y las columnas después.","selectedRowDesc":"Proporciona Datos de la Fila Seleccionada Actualmente, Indicando la Fila que Desencadena un Suceso de Clic Si el Usuario Pulsa un Botón/Link en la Fila","selectedRowsDesc":"Útil en el modo de selección múltiple, igual que SelectedRow","pageNoDesc":"Página de visualización actual, empezando por 1","pageSizeDesc":"Cuántas filas por página","sortColumnDesc":"El nombre de la columna ordenada actualmente seleccionada","sortDesc":"Si la fila actual está en orden descendente","pageOffsetDesc":"El inicio actual de la paginación, utilizado para paginar para obtener datos. Ejemplo: Select * from Usuarios Limit \\'{{table1.pageSize}}\\Desplazamiento{{table1.pageOffset}}\\'","displayDataDesc":"Datos mostrados en la tabla actual","selectedIndexDesc":"Índice seleccionado en Mostrar datos","filterDesc":"Parámetros de filtrado de tablas","dataDesc":"Los datos JSON de la tabla","saveChanges":"Guardar cambios","cancelChanges":"Cancelar cambios","rowSelectChange":"Cambio de selección de fila","rowClick":"Fila Clic","rowExpand":"Fila Ampliar","filterChange":"Cambio de filtro","sortChange":"Ordenar Cambio","pageChange":"Cambio de página","refresh":"Actualiza","rowColor":"Color de fila condicional","rowColorDesc":"Establece Condicionalmente el Color de la Fila en Función de las Variables Opcionales: FilaActual, ÍndiceOriginalActual, ÍndiceActual, TítuloDeColumna. Por ejemplo \\'{{ filaactual.id > 3 ? \"verde%r@\\\" : \"rojo%r@\\\" }}\\'","cellColor":"Color de celda condicional","cellColorDesc":"Establece condicionalmente el color de la celda en función del valor de la celda utilizando CeldaActual. Por ejemplo \\'{{ celdaactual == 3 ? \"verde%r@\\\" : \"rojo%r@\\\" }}\\'","saveChangesNotBind":"No se ha configurado ningún controlador de eventos para guardar los cambios. Por favor, vincula al menos un controlador de eventos antes de hacer clic.","dynamicColumn":"Utilizar la configuración dinámica de columnas","dynamicColumnConfig":"Ajuste de columna","dynamicColumnConfigDesc":"Configuración Dinámica de Columnas. Acepta una Matriz de Nombres de Columna. Todas las Columnas son Visibles por Defecto. Ejemplo: [%r@\\\"id%r@\\\", %r@\\\"name%r@\\\"]","position":"Posición","showDataLoadSpinner":"Mostrar la rueda giratoria durante la carga de datos","showValue":"Mostrar valor","expandable":"Ampliable","configExpandedView":"Configurar vista ampliada","toUpdateRowsDesc":"Una matriz de objetos para filas a actualizar en tablas editables.","empty":"Vacío","falseValues":"Texto cuando es falso","allColumn":"Todos","visibleColumn":"Visible","emptyColumns":"Actualmente no hay columnas visibles"},"image":{"src":"Fuente de la imagen","srcDesc":"La fuente de la imagen. Puede ser una URL, una ruta o una cadena Base64. Por ejemplo: data:image/png;base64, AAA... CCC","supportPreview":"Soporte Haz clic en Vista previa (zoom)","supportPreviewTip":"Efectivo cuando la fuente de la imagen es válida"},"progress":{"value":"Valor","valueTooltip":"El Porcentaje Completo como valor entre 0 y 100","showInfo":"Mostrar valor","valueDesc":"Valor de progreso actual, de 0 a 100","showInfoDesc":"Si mostrar el valor de progreso actual"},"fileViewer":{"invalidURL":"Introduce una URL válida o una cadena Base64","src":"URI del archivo","srcTooltip":"Previsualiza el contenido del enlace proporcionado incrustando HTML, también se pueden admitir datos codificados en base64, por ejemplo: data:application/pdf; base64,AAA... CCC","srcDesc":"La URI del archivo"},"divider":{"title":"Título","align":"Alineación","dashed":"Guiones","dashedDesc":"Si utilizar línea discontinua","titleDesc":"Título del divisor","alignDesc":"Alineación del título del divisor"},"QRCode":{"value":"Valor del contenido del código QR","valueTooltip":"El valor contiene un máximo de 2953 caracteres. El valor del código QR puede codificar varios tipos de datos, como mensajes de texto, URL, datos de contacto (VCard/meCard), credenciales de inicio de sesión Wi-Fi, direcciones de correo electrónico, números de teléfono, mensajes SMS, coordenadas de geolocalización, detalles de eventos del calendario, información de pago, direcciones de criptomonedas y enlaces de descarga de aplicaciones.","valueDesc":"El valor del contenido del código QR","level":"Nivel de tolerancia a fallos","levelTooltip":"Se refiere a la capacidad del código QR para ser escaneado aunque parte de él esté bloqueada. Cuanto más alto es el nivel, más complejo es el código.","includeMargin":"Mostrar margen","image":"Mostrar imagen en el centro","L":"L (Bajo)","M":"M (Medio)","Q":"Q (Cuartil)","H":"H (Alto)","maxLength":"El contenido es demasiado largo. Ajusta la longitud a menos de 2953 caracteres"},"jsonExplorer":{"indent":"Sangría de cada nivel","expandToggle":"Expandir árbol JSON","theme":"Tema del color","valueDesc":"Datos JSON actuales","default":"Por defecto","defaultDark":"Por defecto Oscuro","neutralLight":"Luz neutra","neutralDark":"Neutro Oscuro","azure":"Azure","darkBlue":"Azul oscuro"},"audio":{"src":"URI de la fuente de audio o cadena Base64","defaultSrcUrl":"https://cdn.pixabay.com/audio/2023/07/06/audio_e12e5bea9d.mp3","autoPlay":"Reproducción automática","loop":"Bucle","srcDesc":"URI de audio actual o cadena Base64 como data:audio/mpeg;base64,AAA... CCC","play":"Juega a","playDesc":"Se activa cuando se reproduce audio","pause":"Pausa","pauseDesc":"Se activa cuando se pausa el audio","ended":"Finalizado","endedDesc":"Se activa cuando el audio termina de reproducirse"},"video":{"src":"URI de la fuente de vídeo o cadena Base64","defaultSrcUrl":"https://www.youtube.com/watch?v=pRpeEdMmmQ0","poster":"URL del póster","defaultPosterUrl":"","autoPlay":"Reproducción automática","loop":"Bucle","controls":"Ocultar controles","volume":"Volumen","playbackRate":"Velocidad de reproducción","posterTooltip":"El valor por defecto es el primer fotograma del vídeo","autoPlayTooltip":"Después de cargar el vídeo, se reproducirá automáticamente. Si cambias este valor de Verdadero a Falso, el vídeo se pausará. (Si se establece un Póster, se reproducirá mediante el Botón de Póster)","controlsTooltip":"Ocultar controles de reproducción de vídeo. Puede no ser totalmente compatible con todas las fuentes de vídeo.","volumeTooltip":"Ajustar el Volumen del Reproductor, Entre 0 y 1","playbackRateTooltip":"Ajustar la velocidad del reproductor, entre 1 y 2","srcDesc":"URI de audio actual o cadena Base64 como data:video/mp4;base64, AAA... CCC","play":"Juega a","playDesc":"Se activa cuando se reproduce el vídeo","pause":"Pausa","pauseDesc":"Se activa al pausar el vídeo","load":"Carga","loadDesc":"Se activa cuando el recurso de vídeo ha terminado de cargarse","ended":"Finalizado","endedDesc":"Se activa cuando el vídeo termina de reproducirse","currentTimeStamp":"La posición actual de reproducción del vídeo en segundos","duration":"La duración total del vídeo en segundos"},"media":{"playDesc":"Inicia la reproducción del medio.","pauseDesc":"Pausa la reproducción multimedia.","loadDesc":"Restablece el Medio al Principio y Reinicia Seleccionando el Recurso Multimedia.","seekTo":"Buscar hasta el número de segundos dado, o fracción si la cantidad está entre 0 y 1","seekToAmount":"Número de segundos, o fracción si está entre 0 y 1","showPreview":"Avance del espectáculo"},"rangeSlider":{"start":"Valor inicial","end":"Valor final","step":"Tamaño del paso","stepTooltip":"Granularidad del deslizador, el valor debe ser mayor que 0 y divisible por (Max-Min)"},"iconControl":{"selectIcon":"Selecciona un icono","insertIcon":"Insertar un icono","insertImage":"Insertar una imagen o "},"millisecondsControl":{"timeoutTypeError":"Por favor, introduce el periodo de tiempo de espera correcto en ms, la entrada actual es: {valor}","timeoutLessThanMinError":"La entrada debe ser mayor que {izquierda}, la entrada actual es: {valor}"},"selectionControl":{"single":"Individual","multiple":"Múltiple","close":"Cerrar","mode":"Seleccionar modo"},"container":{"title":"Título del contenedor mostrado"},"drawer":{"closePosition": "Colocación de los cerca","placement":"Colocación de los cajones","size":"Talla","top":"Arriba","right":"A la derecha","bottom":"Fondo","left":"Izquierda","widthTooltip":"Píxel o Porcentaje, por ejemplo 520, 60%.","heightTooltip":"Píxel, por ejemplo 378","openDrawerDesc":"Cajón abierto","closeDrawerDesc":"Cerrar cajón","width":"Ancho del cajón","height":"Altura del cajón"},"meeting":{"logLevel":"Agora SDK Nivel de registro","placement":"Colocación del cajón de reunión","meeting":"Ajustes de la reunión","cameraView":"Vista de cámara","cameraViewDesc":"Vista de cámara del usuario local (anfitrión)","screenShared":"Pantalla compartida","screenSharedDesc":"Pantalla compartida por el usuario local (anfitrión)","audioUnmuted":"Audio sin silenciar","audioMuted":"Audio silenciado","videoClicked":"Vídeo pulsado","videoOff":"Vídeo apagado","videoOn":"Vídeo","size":"Talla","top":"Arriba","host":"Anfitrión de la Sala de Reuniones. Tendrías que gestionar el anfitrión como una Aplicación Lógica propia","participants":"Participantes de la Sala de Reuniones","shareScreen":"Pantalla compartida por el usuario local","appid":"ID de la aplicación Ágora","meetingName":"Nombre de la reunión","localUserID":"ID de usuario del host","userName":"Nombre de usuario del host","rtmToken":"Ficha Agora RTM","rtcToken":"Ficha Agora RTC","noVideo":"Sin vídeo","profileImageUrl":"URL de la imagen del perfil","right":"A la derecha","bottom":"Fondo","videoId":"ID del flujo de vídeo","audioStatus":"Estado del audio","left":"Izquierda","widthTooltip":"Píxel o Porcentaje, por ejemplo 520, 60%.","heightTooltip":"Píxel, por ejemplo 378","openDrawerDesc":"Cajón abierto","closeDrawerDesc":"Cerrar cajón","width":"Ancho del cajón","height":"Altura del cajón","actionBtnDesc":"Botón de acción","broadCast":"Transmitir mensajes","title":"Título de la reunión","meetingCompName":"Controlador de Reuniones Agora","sharingCompName":"Compartir pantalla Stream","videoCompName":"Flujo de cámara","videoSharingCompName":"Compartir pantalla Stream","meetingControlCompName":"Botón de control","meetingCompDesc":"Componente de la reunión","meetingCompControls":"Control de reuniones","meetingCompKeywords":"Reunión Ágora, Reunión Web, Colaboración","iconSize":"Tamaño del icono","userId":"ID de usuario del host","roomId":"Identificación de la habitación","meetingActive":"Reunión en curso","messages":"Mensajes emitidos"},"settings":{"title":"Ajustes","userGroups":"Grupos de usuarios","organization":"Espacios de trabajo","audit":"Registros de auditoría","theme":"Temas","plugin":"Plugins","advanced":"Avanzado","lab":"Laboratorio","branding":"Marca","oauthProviders":"Proveedores OAuth","appUsage":"Registros de uso de la aplicación","environments":"Entornos","premium":"Premium"},"memberSettings":{"admin":"Admin","adminGroupRoleInfo":"El administrador puede gestionar los miembros y recursos del grupo","adminOrgRoleInfo":"Los administradores son propietarios de todos los recursos y pueden gestionar grupos.","member":"Miembro","memberGroupRoleInfo":"Los miembros pueden ver a los miembros del grupo","memberOrgRoleInfo":"Los miembros sólo pueden utilizar o visitar los recursos a los que tienen acceso.","title":"Miembros","createGroup":"Crear grupo","newGroupPrefix":"Nuevo Grupo ","allMembers":"Todos los miembros","deleteModalTitle":"Eliminar este grupo","deleteModalContent":"No se puede restaurar el grupo eliminado. ¿Estás Seguro de Borrar el Grupo?","addMember":"Añadir miembros","nameColumn":"Nombre de usuario","joinTimeColumn":"Hora de incorporación","actionColumn":"Operación","roleColumn":"Papel","exitGroup":"Grupo de salida","moveOutGroup":"Eliminar del Grupo","inviteUser":"Invitar a miembros","exitOrg":"Deja","exitOrgDesc":"¿Estás seguro de que quieres dejar este espacio de trabajo?","moveOutOrg":"Elimina","moveOutOrgDescSaasMode":"¿Estás seguro de que quieres eliminar al usuario {nombre} de este espacio de trabajo?","moveOutOrgDesc":"¿Estás seguro de que quieres eliminar al usuario {nombre}? Esta acción no se puede recuperar.","devGroupTip":"Los miembros del Grupo de Desarrolladores tienen privilegios para crear aplicaciones y fuentes de datos.","lastAdminQuit":"El último administrador no puede salir.","organizationNotExist":"El espacio de trabajo actual no existe","inviteUserHelp":"Puedes copiar el enlace de invitación para enviarlo al usuario","inviteUserLabel":"Enlace de invitación:","inviteCopyLink":"Copiar enlace","inviteText":"{userName} Te invita a unirte al espacio de trabajo \"{organization}%r@\\\", Haz clic en el enlace para unirte: {inviteLink}","groupName":"Nombre del grupo","createTime":"Crear Tiempo","manageBtn":"Gestiona","userDetail":"Detalle","syncDeleteTip":"Este grupo ha sido eliminado de la Agenda Fuente","syncGroupTip":"Este grupo es un grupo de sincronización de la Agenda y no se puede editar"},"orgSettings":{"newOrg":"Nuevo espacio de trabajo (Organización)","title":"Espacio de trabajo","createOrg":"Crear espacio de trabajo (Organización)","deleteModalTitle":"¿Estás seguro de eliminar este espacio de trabajo?","deleteModalContent":"Estás a punto de Eliminar este Espacio de Trabajo {permanentementeEliminado}. Una vez Eliminado, el Espacio de Trabajo {noSeRestaura}.","permanentlyDelete":"Permanentemente","notRestored":"No se puede restaurar","deleteModalLabel":"Introduce el nombre del espacio de trabajo {nombre} para confirmar la operación:","deleteModalTip":"Introduce el nombre del espacio de trabajo","deleteModalErr":"El nombre del espacio de trabajo es incorrecto","deleteModalBtn":"Borra","editOrgTitle":"Editar información del espacio de trabajo","orgNameLabel":"Nombre del espacio de trabajo:","orgNameCheckMsg":"El nombre del espacio de trabajo no puede estar vacío","orgLogo":"Logotipo del Espacio de Trabajo:","logoModify":"Modificar imagen","inviteSuccessMessage":"Únete con éxito al espacio de trabajo","inviteFailMessage":"Error al unirse al espacio de trabajo","uploadErrorMessage":"Error de carga","orgName":"Nombre del espacio de trabajo"},"freeLimit":"Prueba gratuita","tabbedContainer":{"switchTab":"Pestaña Interruptor","switchTabDesc":"Se activa al cambiar de pestaña","tab":"Fichas","atLeastOneTabError":"El Contenedor de Pestañas Guarda al Menos Una Pestaña","selectedTabKeyDesc":"Pestaña \"Seleccionado actualmente","iconPosition":"Icono Posición"},"formComp":{"containerPlaceholder":"Arrastra componentes desde el panel derecho o","openDialogButton":"Generar un Formulario a partir de una de tus Fuentes de Datos","resetAfterSubmit":"Reiniciar después de enviar correctamente","initialData":"Datos iniciales","disableSubmit":"Desactivar Enviar","success":"Formulario generado correctamente","selectCompType":"Selecciona el tipo de componente","dataSource":"Fuente de datos: ","selectSource":"Seleccionar fuente","table":"Tabla: ","selectTable":"Seleccionar tabla","columnName":"Nombre de la columna","dataType":"Tipo de datos","compType":"Tipo de componente","required":"Necesario","generateForm":"Generar formulario","compSelectionError":"Tipo de columna no configurada","compTypeNameError":"No se ha podido obtener el nombre del tipo de componente","noDataSourceSelected":"No se ha seleccionado ninguna fuente de datos","noTableSelected":"No hay mesa seleccionada","noColumn":"Sin columna","noColumnSelected":"Sin columna seleccionada","noDataSourceFound":"No se ha encontrado ninguna fuente de datos compatible. Crear una nueva fuente de datos","noTableFound":"No se encontraron tablas en esta fuente de datos, por favor selecciona otra fuente de datos","noColumnFound":"No se ha encontrado ninguna columna compatible en esta tabla. Selecciona otra tabla","formTitle":"Título del formulario","name":"Nombre","nameTooltip":"El Nombre del Atributo en los Datos del Formulario, si se deja en blanco, es por defecto el Nombre del Componente","notSupportMethod":"Métodos no admitidos: ","notValidForm":"El formulario no es válido","resetDesc":"Restablecer los datos del formulario al valor por defecto","clearDesc":"Borrar datos del formulario","setDataDesc":"Establecer datos del formulario","valuesLengthError":"Número de parámetro Error","valueTypeError":"Tipo de parámetro Error","dataDesc":"Datos del formulario actual","loadingDesc":"¿Si el formulario está cargando?"},"modalComp":{"close":"Cerrar","closeDesc":"Se activa cuando se cierra el cuadro de diálogo modal","openModalDesc":"Abrir el Cuadro de Diálogo","closeModalDesc":"Cerrar el Cuadro de Diálogo","visibleDesc":"¿Es Visible? Si es Verdadero, Aparecerá el Cuadro de Diálogo Actual","modalHeight":"Altura modal","modalHeightTooltip":"Píxel, Ejemplo: 222","modalWidth":"Anchura modal","modalWidthTooltip":"Número o porcentaje, Ejemplo: 520, 60%"},"listView":{"noOfRows":"Recuento de filas","noOfRowsTooltip":"Número de filas de la lista - Suele establecerse en una variable (por ejemplo, \\'{{query1.data.length}}\\') para presentar los resultados de la consulta","noOfColumns":"Recuento de columnas","itemIndexName":"Elemento de datos Índice Nombre","itemIndexNameDesc":"El nombre de la variable que se refiere al índice del elemento, por defecto como {por defecto}.","itemDataName":"Elemento de datos Nombre del objeto","itemDataNameDesc":"El nombre de la variable que se refiere al objeto de datos del elemento, por defecto como {default}.","itemsDesc":"Exponer datos de componentes en lista","dataDesc":"Los datos JSON utilizados en la lista actual","dataTooltip":"Si sólo pones un Número, Este Campo Se Considerará Como Recuento De Filas, Y Los Datos Se Considerarán Vacíos."},"navigation":{"addText":"Añadir elemento de submenú","logoURL":"Navegación Logo URL","horizontalAlignment":"Alineación horizontal","logoURLDesc":"Puedes mostrar un Logotipo en el lado izquierdo introduciendo un Valor URI o una Cadena Base64 como data:image/png;base64,AAA... CCC","itemsDesc":"Elementos del menú de navegación jerárquica"},"droppadbleMenuItem":{"subMenu":"Submenú {número}"},"navItemComp":{"active":"Activo"},"iframe":{"URLDesc":"La URL de origen del contenido del IFrame. Asegúrate de que la URL es HTTPS o localhost. Asegúrate también de que la URL no está bloqueada por la Política de Seguridad de Contenidos (CSP) del navegador. La cabecera \\'X-Frame-Options\\' no debe tener el valor \\'DENY\\' o \\'SAMEORIGIN\\'.","allowDownload":"Permitir descargas","allowSubmitForm":"Permitir Enviar Formulario","allowMicrophone":"Permitir micrófono","allowCamera":"Permitir cámara","allowPopup":"Permitir ventanas emergentes"},"switchComp":{"defaultValue":"Valor booleano por defecto","open":"En","close":"Fuera de","openDesc":"Se activa al encender el interruptor","closeDesc":"Se activa cuando el interruptor está apagado","valueDesc":"Estado actual del interruptor"},"signature":{"tips":"Texto de sugerencia","signHere":"Firma aquí","showUndo":"Mostrar Deshacer","showClear":"Mostrar Borrar"},"localStorageComp":{"valueDesc":"Todos los datos almacenados actualmente","setItemDesc":"Añadir un elemento","removeItemDesc":"Eliminar un elemento","clearItemDesc":"Borrar todos los artículos"},"utilsComp":{"openUrl":"Abrir URL","openApp":"Abrir App","copyToClipboard":"Copiar al portapapeles","downloadFile":"Descargar archivo"},"messageComp":{"info":"Enviar una notificación","success":"Enviar una notificación de éxito","warn":"Enviar una notificación de advertencia","error":"Enviar una notificación de error"},"themeComp":{"switchTo":"Cambiar tema"},"transformer":{"preview":"Vista previa","docLink":"Leer más sobre Transformers...","previewSuccess":"Vista previa Éxito","previewFail":"Vista previa Fracaso","deleteMessage":"Eliminar Transformador con éxito. Puedes usar {undoKey} para Deshacer.","documentationText":"Los Transformadores están diseñados para transformar datos y reutilizar tu código JavaScript multilínea. Utiliza Transformadores para adaptar datos de consultas o componentes a las necesidades de tu App local. A diferencia de la consulta JavaScript, el transformador está diseñado para realizar operaciones de sólo lectura, lo que significa que no puedes lanzar una consulta o actualizar un estado temporal dentro de un transformador."},"temporaryState":{"value":"Valor inicial","valueTooltip":"El valor inicial almacenado en el estado temporal puede ser cualquier valor JSON válido.","docLink":"Leer más sobre los Estados Temporales...","pathTypeError":"La ruta debe ser una cadena o una matriz de valores","unStructuredError":"Los datos no estructurados {prev} no pueden ser actualizados por {path}.","valueDesc":"Valor Temporal del Estado","deleteMessage":"El Estado Temporal se Borra con Éxito. Puedes Usar {undoKey} para Deshacer.","documentationText":"Los estados temporales en Lowcoder son una potente función utilizada para gestionar variables complejas que actualizan dinámicamente el estado de los componentes de tu aplicación. Estos estados actúan como almacenamiento intermedio o transitorio de datos que pueden cambiar con el tiempo debido a interacciones del usuario u otros procesos."},"dataResponder":{"data":"Datos","dataDesc":"Datos del respondedor de datos actual","dataTooltip":"Cuando se modifiquen estos datos, se desencadenarán acciones posteriores.","docLink":"Más información sobre los respondedores de datos...","deleteMessage":"El Respondedor de datos se ha eliminado correctamente. Puedes utilizar {undoKey} para Deshacer.","documentationText":"Al desarrollar una aplicación, puedes asignar eventos a los componentes para controlar los cambios en datos concretos. Por ejemplo, un componente Tabla puede tener eventos como %r@\\\"Cambio de selección de fila%r@\\\", %r@\\\"Cambio de filtro%r@\\\", %r@\\\"Cambio de ordenación%r@\\\" y %r@\\\"Cambio de página%r@\\\" para controlar los cambios en la propiedad Fila seleccionada. Sin embargo, para los cambios en estados temporales, transformadores o resultados de consulta, en los que no se dispone de eventos estándar, se utilizan los Respondedores de datos. Te permiten detectar y reaccionar ante cualquier modificación de los datos."},"theme":{"title":"Temas","createTheme":"Crear tema","themeName":"Nombre del tema:","themeNamePlaceholder":"Introduce un nombre de tema","defaultThemeTip":"Tema por defecto:","createdThemeTip":"El tema que has creado:","option":"Opción{índice}","input":"Entrada","confirm":"Ok","emptyTheme":"No hay temas disponibles","click":"","toCreate":"","nameColumn":"Nombre","defaultTip":"Por defecto","updateTimeColumn":"Hora de actualización","edit":"Edita","cancelDefaultTheme":"Desactivar tema por defecto","setDefaultTheme":"Establecer como tema por defecto","copyTheme":"Tema duplicado","setSuccessMsg":"Ajuste superado","cancelSuccessMsg":"Desajuste conseguido","deleteSuccessMsg":"Supresión superada","checkDuplicateNames":"El nombre del tema ya existe, por favor, introdúcelo de nuevo","copySuffix":" Copia","saveSuccessMsg":"Guardado correctamente","leaveTipTitle":"Consejos","leaveTipContent":"¿Aún no te has salvado, confirma que te vas?","leaveTipOkText":"Deja","goList":"Volver a la lista","saveBtn":"Guarda","mainColor":"Colores principales","text":"Colores del texto","defaultTheme":"Por defecto","yellow":"Amarillo","green":"Verde","previewTitle":"Vista previa del tema\\nComponentes de ejemplo que utilizan los colores de tu tema","dateColumn":"Fecha","emailColumn":"Envía un correo electrónico a","phoneColumn":"Teléfono","subTitle":"Título","linkLabel":"Enlace","linkUrl":"app.lowcoder.nube","progressLabel":"Progreso","sliderLabel":"Deslizador","radioLabel":"Radio","checkboxLabel":"Casilla de verificación","buttonLabel":"Botón Formulario","switch":"Interruptor","previewDate":"16/10/2022","previewEmail1":"ted.com","previewEmail2":"skype.com","previewEmail3":"imgur.com","previewEmail4":"globo.com","previewPhone1":"+63-317-333-0093","previewPhone2":"+30-668-580-6521","previewPhone3":"+86-369-925-2071","previewPhone4":"+7-883-227-8093","chartPreviewTitle":"Vista previa del estilo de gráfico","chartSpending":"Gastar","chartBudget":"Presupuesto","chartAdmin":"Administración","chartFinance":"Finanzas","chartSales":"Ventas","chartFunnel":"Gráfico de embudo","chartShow":"Mostrar","chartClick":"Haz clic en","chartVisit":"Visita","chartQuery":"Consulta","chartBuy":"Comprar"},"pluginSetting":{"title":"Plugins","npmPluginTitle":"Plugins npm","npmPluginDesc":"Configurar plugins npm para todas las aplicaciones del espacio de trabajo actual.","npmPluginEmpty":"No se han añadido plugins npm.","npmPluginAddButton":"Añadir un plugin npm","saveSuccess":"Guardado correctamente"},"advanced":{"title":"Avanzado","defaultHomeTitle":"Página de inicio por defecto","defaultHomeHelp":"La página de inicio es la aplicación que todos los no desarrolladores verán por defecto cuando se conecten. Nota: Asegúrate de que la aplicación seleccionada es accesible para los no desarrolladores.","defaultHomePlaceholder":"Selecciona la página de inicio predeterminada","saveBtn":"Guarda","preloadJSTitle":"Precargar JavaScript","preloadJSHelp":"Configurar código JavaScript precargado para todas las aplicaciones del espacio de trabajo actual.","preloadCSSTitle":"Precargar CSS","preloadCSSHelp":"Configura el código CSS precargado para todas las aplicaciones del espacio de trabajo actual.","preloadCSSApply":"Aplicar a la página de inicio del espacio de trabajo","preloadLibsTitle":"Biblioteca JavaScript","preloadLibsHelp":"Configura Bibliotecas JavaScript Precargadas para Todas las Aplicaciones en el Espacio de Trabajo Actual, y el Sistema Tiene Incorporadas lodash, day.js, uuid, numbro para Uso Directo. Las Bibliotecas JavaScript Se Cargan Antes De Inicializar La Aplicación, Por Lo Que Hay Un Cierto Impacto En El Rendimiento De La Aplicación.","preloadLibsEmpty":"No se han añadido bibliotecas JavaScript","preloadLibsAddBtn":"Añadir una biblioteca","saveSuccess":"Guardado correctamente","AuthOrgTitle":"Pantalla de bienvenida al espacio de trabajo","AuthOrgDescrition":"La URL para que tus usuarios inicien sesión en el espacio de trabajo actual."},"branding":{"title":"Marca","logoTitle":"Logotipo","logoHelp":"Sólo .JPG, .SVG o .PNG","faviconTitle":"Favicon","faviconHelp":"Sólo .JPG, .SVG o .PNG","brandNameTitle":"Marca","headColorTitle":"Color de la cabeza","save":"Guarda","saveSuccessMsg":"Guardado correctamente","upload":"Haz clic para cargar"},"networkMessage":{"0":"No se ha podido conectar con el servidor, comprueba la red","401":"Autenticación fallida, por favor, inicia sesión de nuevo","403":"Sin permiso, ponte en contacto con el administrador para obtener autorización","500":"Servicio ocupado, inténtalo más tarde","timeout":"Tiempo de espera de la solicitud"},"share":{"title":"Comparte","viewer":"Visor","editor":"Editor","owner":"Propietario","datasourceViewer":"Se puede utilizar","datasourceOwner":"Puede gestionar"},"debug":{"title":"Título","switch":"Componente del interruptor: "},"module":{"emptyText":"Sin datos","circularReference":"Referencia circular, ¡no se puede utilizar el módulo/aplicación actual!","emptyTestInput":"El módulo actual no tiene entrada para comprobar","emptyTestMethod":"El módulo actual no tiene ningún método para probar","name":"Nombre","input":"Entrada","params":"Parámetros","emptyParams":"No se ha añadido ningún parámetro","emptyInput":"No se ha añadido ninguna entrada","emptyMethod":"No se ha añadido ningún método","emptyOutput":"No se ha añadido ninguna salida","data":"Datos","string":"Cadena","number":"Número","array":"Matriz","boolean":"Booleano","query":"Consulta","autoScaleCompHeight":"Básculas de altura de componentes con contenedor","excuteMethod":"Ejecutar método {nombre}","method":"Método","action":"Acción","output":"Salida","nameExists":"Nombre {name} Ya existe","eventTriggered":"Evento {nombre} activado","globalPromptWhenEventTriggered":"Muestra un aviso global cuando se activa un evento","emptyEventTest":"El módulo actual no tiene eventos que probar","emptyEvent":"No se ha añadido ningún evento","event":"Evento"},"resultPanel":{"returnFunction":"El valor de retorno es una función.","consume":"{tiempo}","JSON":"Mostrar JSON"},"createAppButton":{"creating":"Crear...","created":"Crear {nombre}"},"apiMessage":{"authenticationFail":"Ha fallado la autenticación de usuario, por favor, inicia sesión de nuevo","verifyAccount":"Necesidad de verificar la cuenta","functionNotSupported":"La versión actual no soporta esta función. Ponte en contacto con el equipo comercial de Lowcoder para actualizar tu cuenta."},"globalErrorMessage":{"createCompFail":"Crear componente {comp} Fallido","notHandledError":"{método} Método no ejecutado"},"aggregation":{"navLayout":"Barra de navegación","chooseApp":"Elegir aplicación","iconTooltip":"Admite enlace src de imagen o cadena base64 como data:image/png;base64,AAA... CCC","hideWhenNoPermission":"Oculto para usuarios no autorizados","queryParam":"Parámetros de consulta URL","hashParam":"Parámetros Hash de URL","tabBar":"Barra de pestañas","emptyTabTooltip":"Configurar esta página en el panel derecho"},"appSetting":{"450":"450px (Teléfono)","800":"800px (Tableta)","1440":"1440px (Portátil)","1920":"1920px (Pantalla ancha)","3200":"3200px (Pantalla supergrande)","title":"Configuración general de la aplicación","autofill":"Autorrelleno","userDefined":"Personalizado","default":"Por defecto","tooltip":"Cerrar la ventana emergente después de ajustar","canvasMaxWidth":"Ancho máximo del lienzo para esta aplicación","userDefinedMaxWidth":"Ancho máximo personalizado","inputUserDefinedPxValue":"Introduce un valor de píxel personalizado","maxWidthTip":"La anchura máxima debe ser mayor o igual que 350","themeSetting":"Tema Estilo Aplicado","themeSettingDefault":"Por defecto","themeCreate":"Crear tema"},"customShortcut":{"title":"Atajos personalizados","shortcut":"Atajo","action":"Acción","empty":"Sin atajos","placeholder":"Pulsa Atajo","otherPlatform":"Otros","space":"Espacio"},"profile":{"orgSettings":"Configuración del espacio de trabajo","switchOrg":"Cambiar de espacio de trabajo","joinedOrg":"Mis espacios de trabajo","createOrg":"Crear espacio de trabajo","logout":"Cerrar sesión","personalInfo":"Mi perfil","bindingSuccess":"Vinculación {nombreFuente} Éxito","uploadError":"Error de carga","editProfilePicture":"Modifica","nameCheck":"El nombre no puede estar vacío","name":"Nombre: ","namePlaceholder":"Introduce tu nombre","toBind":"Encuadernar","binding":"Es vinculante","bindError":"Error de parámetro, actualmente no admitido Vinculación.","bindName":"Enlazar {nombre}","loginAfterBind":"Después de vincular, puedes utilizar {nombre} para iniciar sesión","bindEmail":"Enlazar correo electrónico:","email":"Envía un correo electrónico a","emailCheck":"Introduce una dirección de correo electrónico válida","emailPlaceholder":"Introduce tu dirección de correo electrónico","submit":"Envía","bindEmailSuccess":"Éxito de la encuadernación por correo electrónico","passwordModifiedSuccess":"Contraseña cambiada correctamente","passwordSetSuccess":"Contraseña establecida correctamente","oldPassword":"Contraseña antigua:","inputCurrentPassword":"Introduce tu contraseña actual","newPassword":"Nueva contraseña:","inputNewPassword":"Introduce tu nueva contraseña","confirmNewPassword":"Confirma la nueva contraseña:","inputNewPasswordAgain":"Vuelve a introducir tu nueva contraseña","password":"Contraseña:","modifyPassword":"Modificar contraseña","setPassword":"Establecer contraseña","alreadySetPassword":"Conjunto de contraseñas","setPassPlaceholder":"Puedes iniciar sesión con tu contraseña","setPassAfterBind":"Puedes establecer la contraseña después de vincular la cuenta","socialConnections":"Conexiones sociales"},"shortcut":{"shortcutList":"Atajos de teclado","click":"Haz clic en","global":"Global","toggleShortcutList":"Alternar atajos de teclado","editor":"Editor","toggleLeftPanel":"Alternar panel izquierdo","toggleBottomPanel":"Alternar panel inferior","toggleRightPanel":"Alternar panel derecho","toggleAllPanels":"Conmutar todos los paneles","preview":"Vista previa","undo":"Deshacer","redo":"Rehaz","showGrid":"Mostrar cuadrícula","component":"Componente","multiSelect":"Seleccionar varios","selectAll":"Seleccionar todo","copy":"Copia","cut":"Corta","paste":"Pega","move":"Muévete","zoom":"Cambia el tamaño de","delete":"Borra","deSelect":"Deselecciona","queryEditor":"Editor de consultas","excuteQuery":"Ejecutar consulta actual","editBox":"Editor de texto","formatting":"Formato","openInLeftPanel":"Abrir en el panel izquierdo"},"help":{"videoText":"Visión general","onBtnText":"OK","permissionDenyTitle":"💡 ¿No se puede crear una nueva aplicación o fuente de datos?","permissionDenyContent":"No tienes permiso para crear la aplicación y la fuente de datos. Ponte en contacto con el Administrador para unirte al Grupo de Desarrolladores.","appName":"Aplicación Tutorial","chat":"Chatea con nosotros","docs":"Ver documentación","editorTutorial":"Tutorial del editor","update":"¿Qué hay de nuevo?","version":"Versión","versionWithColon":"Versión: ","submitIssue":"Enviar un asunto"},"header":{"nameCheckMessage":"El nombre no puede estar vacío","viewOnly":"Ver sólo","recoverAppSnapshotTitle":"¿Restaurar esta versión?","recoverAppSnapshotContent":"Restaurar la aplicación actual a la versión creada en {tiempo}.","recoverAppSnapshotMessage":"Restaurar esta versión","returnEdit":"Volver al editor","deploy":"Publica","export":"Exportar a JSON","editName":"Editar nombre","duplicate":"Duplicar {tipo}","snapshot":"Historia","scriptsAndStyles":"Guiones y estilo","appSettings":"Ajustes de la aplicación","preview":"Vista previa","editError":"Modo de Vista Previa de la Historia, no se admite ninguna operación.","clone":"Clon","editorMode_layout":"Disposición","editorMode_logic":"Lógica","editorMode_both":"Ambos"},"userAuth":{"registerByEmail":"Inscríbete","email":"Correo electrónico:","inputEmail":"Introduce tu dirección de correo electrónico","inputValidEmail":"Introduce una dirección de correo electrónico válida","register":"Inscríbete","userLogin":"Regístrate","login":"Regístrate","bind":"Encuaderna","passwordCheckLength":"Al menos {min} Personajes","passwordCheckContainsNumberAndLetter":"Debe contener letras y números","passwordCheckSpace":"No puede contener espacios en blanco","welcomeTitle":"Bienvenido a {productName}","inviteWelcomeTitle":"{username} Te invito a conectarte {productName}","terms":"Términos","privacy":"Política de privacidad","registerHint":"He leído y acepto el","chooseAccount":"Elige tu cuenta","signInLabel":"Iniciar sesión con {nombre}","bindAccount":"Vincular cuenta","scanQrCode":"Escanea el código QR con {nombre}","invalidThirdPartyParam":"Parámetros de terceros no válidos","account":"Cuenta","inputAccount":"Introduce tu cuenta","ldapLogin":"Inicio de sesión LDAP","resetPassword":"Restablecer contraseña","resetPasswordDesc":"Restablecer la contraseña del usuario {nombre}. Se generará una nueva contraseña después de restablecerla.","resetSuccess":"Reinicio efectuado","resetSuccessDesc":"Se ha restablecido la contraseña. La nueva contraseña es: {contraseña}","copyPassword":"Copiar contraseña","poweredByLowcoder":"Desarrollado por Lowcoder.cloud"},"preLoad":{"jsLibraryHelpText":"Añade Bibliotecas JavaScript a tu Aplicación Actual mediante Direcciones URL. lodash, day.js, uuid, numbro están Integradas en el Sistema para su Uso Inmediato. Las Bibliotecas JavaScript se Cargan Antes de Inicializar la Aplicación, Lo Que Puede Tener un Impacto en el Rendimiento de la Aplicación.","exportedAs":"Exportado como","urlTooltip":"Dirección URL de la biblioteca JavaScript, se recomienda [unpkg.com](https://unpkg.com/) o [jsdelivr.net](https://www.jsdelivr.com/)","recommended":"Recomendado","viewJSLibraryDocument":"Documento","jsLibraryURLError":"URL no válida","jsLibraryExist":"La biblioteca JavaScript ya existe","jsLibraryEmptyContent":"No se han añadido bibliotecas JavaScript","jsLibraryDownloadError":"Error de descarga de la biblioteca JavaScript","jsLibraryInstallSuccess":"Biblioteca JavaScript instalada correctamente","jsLibraryInstallFailed":"Fallo en la instalación de la biblioteca JavaScript","jsLibraryInstallFailedCloud":"Puede que la biblioteca no esté disponible en el Sandbox, [Documentación](https://docs.lowcoder.cloud/build-apps/write-javascript/use-third-party-libraries#manually-import-libraries)\\n{mensaje}","jsLibraryInstallFailedHost":"{mensaje}","add":"Añadir nuevo","jsHelpText":"Añade un Método o Variable Global a la Aplicación Actual.","cssHelpText":"Añadir Estilos a la Aplicación Actual. La Estructura DOM Puede Cambiar Mientras Itera el Sistema. Intenta Modificar los Estilos a Través de las Propiedades de los Componentes.","scriptsAndStyles":"Guiones y estilos","jsLibrary":"Biblioteca JavaScript"},"editorTutorials":{"component":"Componente","componentContent":"El Panel de Componentes Derecho te ofrece muchos Bloques de Aplicación (Componentes) ya hechos. Puedes arrastrarlos al lienzo para utilizarlos. También puedes crear tus propios componentes con unos pocos conocimientos de programación.","canvas":"Lienzo","canvasContent":"Construye tus aplicaciones en el Lienzo con un enfoque \"Lo que ves es lo que hay\". Sólo tienes que arrastrar y soltar componentes para diseñar tu diseño, y utilizar los atajos de teclado para una edición rápida como borrar, copiar y pegar. Una vez seleccionado un componente, puedes ajustar todos los detalles, desde el estilo y el diseño hasta la vinculación de datos y el comportamiento lógico. Además, disfruta de la ventaja añadida del diseño adaptable, que garantiza que tus aplicaciones se vean bien en cualquier dispositivo.","queryData":"Consulta de datos","queryDataContent":"Aquí puedes crear Consultas de Datos y Conectarte a tu MySQL, MongoDB, Redis, Airtable y muchas Otras Fuentes de Datos. Tras configurar la Consulta, haz clic en \"Ejecutar\" para obtener los Datos y continuar con el Tutorial.","compProperties":"Propiedades de los componentes"},"homeTutorials":{"createAppContent":"🎉 Bienvenido a {productName}, haz clic en \\'App\\' y empieza a crear tu primera aplicación.","createAppTitle":"Crear aplicación"},"history":{"layout":"\\'{0}\\' ajuste del diseño","upgrade":"Actualizar \\'{0}\\'","delete":"Borrar \\'{0}\\'","add":"Añade \"0\".","modify":"Modificar \\'{0}\\'","rename":"Cambia el nombre de \"{1}\" a \"{0}\".","recover":"Recuperar la versión \"2","recoverVersion":"Recuperar versión","andSoOn":"etc.","timeFormat":"MM DD a las hh:mm A","emptyHistory":"Sin antecedentes","currentVersionWithBracket":" (Actual)","currentVersion":"Versión actual","justNow":"Ahora mismo","history":"Historia"},"home":{"allApplications":"Todas las aplicaciones","allModules":"Todos los módulos","allFolders":"Todas las carpetas","modules":"Módulos","module":"Módulo","trash":"Basura","queryLibrary":"Biblioteca de consultas","datasource":"Fuentes de datos","selectDatasourceType":"Selecciona el tipo de fuente de datos","home":"Inicio | Área de Administración","all":"Todos","app":"Aplicación","navigation":"Navegación","navLayout":"Navegación por PC","navLayoutDesc":"Menú a la izquierda para facilitar la navegación por el escritorio.","mobileTabLayout":"Navegación móvil","mobileTabLayoutDesc":"Barra de navegación inferior para una navegación móvil fluida.","folders":"Carpetas","folder":"Carpeta","rootFolder":"Raíz","import":"Importa","export":"Exportar a JSON","inviteUser":"Invitar a miembros","createFolder":"Crear carpeta","createFolderSubTitle":"Nombre de la carpeta:","moveToFolder":"Mover a carpeta","moveToTrash":"Mover a la papelera","moveToFolderSubTitle":"Desplázate a:","folderName":"Nombre de la carpeta:","resCardSubTitle":"{tiempo} por {creador}","trashEmpty":"La papelera está vacía.","projectEmpty":"Aquí no hay nada.","projectEmptyCanAdd":"Todavía no tienes ninguna aplicación. Haz clic en Nueva para empezar.","name":"Nombre","type":"Tipo","creator":"Creado por","lastModified":"Última modificación","deleteTime":"Borrar hora","createTime":"Crear tiempo","datasourceName":"Nombre de la fuente de datos","databaseName":"Nombre de la base de datos","nameCheckMessage":"El nombre no puede estar vacío","deleteElementTitle":"Borrar permanentemente","moveToTrashSubTitle":"{tipo} {nombre} se moverá a la papelera.","deleteElementSubTitle":"Borrar {tipo} {nombre} permanentemente, no se puede recuperar.","deleteSuccessMsg":"Eliminado con éxito","deleteErrorMsg":"Error borrado","recoverSuccessMsg":"Recuperado con éxito","newDatasource":"Nueva fuente de datos","creating":"Crear...","chooseDataSourceType":"Elige el tipo de fuente de datos","folderAlreadyExists":"La carpeta ya existe","newNavLayout":"{nombredeusuario} de {nombre} ","newApp":"nuevo {nombre} de {nombre} de {usuario} ","importError":"Error de importación, {mensaje}","exportError":"Error de exportación, {mensaje}","importSuccess":"Éxito de la importación","fileUploadError":"Error de carga de archivos","fileFormatError":"Error de formato de archivo","groupWithSquareBrackets":"[Grupo] ","allPermissions":"Propietario","shareLink":"Comparte el enlace: ","copyLink":"Copiar enlace","appPublicMessage":"Haz pública la aplicación. Cualquiera puede verla.","modulePublicMessage":"Haz que el módulo sea público. Cualquiera puede verlo.","memberPermissionList":"Permisos de los miembros: ","orgName":"{orgName} admins","addMember":"Añadir miembros","addPermissionPlaceholder":"Introduce un nombre para buscar miembros","searchMemberOrGroup":"Buscar miembros o grupos: ","addPermissionErrorMessage":"Fallo al añadir permiso, {mensaje}","copyModalTitle":"Clonarlo","copyNameLabel":"{tipo} nombre","copyModalfolderLabel":"Añadir a la carpeta","copyNamePlaceholder":"Por favor, introduce un {tipo} de nombre","chooseNavType":"Elige el tipo de navegación","createNavigation":"Crear navegación"},"carousel":{"dotPosition":"Posición de los puntos de navegación","autoPlay":"Reproducción automática","showDots":"Mostrar puntos de navegación"},"npm":{"invalidNpmPackageName":"Nombre o URL de paquete npm no válidos.","pluginExisted":"Este plugin npm ya existía","compNotFound":"No se ha encontrado el componente {compName}.","addPluginModalTitle":"Añadir plugin desde un repositorio npm","pluginNameLabel":"URL o nombre del paquete npm","noCompText":"Sin componentes.","compsLoading":"Cargando...","removePluginBtnText":"Elimina","addPluginBtnText":"Añadir plugin npm"},"toggleButton":{"valueDesc":"El Valor por Defecto del Botón Alternar, Por Ejemplo: Falso","trueDefaultText":"Ocultar","falseDefaultText":"Mostrar","trueLabel":"Texto para Verdadero","falseLabel":"Texto para Falso","trueIconLabel":"Icono de Verdadero","falseIconLabel":"Icono de Falso","iconPosition":"Icono Posición","showText":"Mostrar texto","alignment":"Alineación","showBorder":"Mostrar borde"},"componentDoc":{"markdownDemoText":"**Lowcoder** | Crea aplicaciones de software para tu Empresa y tus Clientes con mínima experiencia en codificación. Lowcoder es la mejor alternativa a Retool, Appsmith o Tooljet.","demoText":"Lowcoder | Crea aplicaciones de software para tu Empresa y tus Clientes con una mínima experiencia en codificación. Lowcoder es la mejor alternativa a Retool, Appsmith o Tooljet.","submit":"Envía","style":"Estilo","danger":"Peligro","warning":"Advertencia","success":"Éxito","menu":"Menú","link":"Enlace","customAppearance":"Apariencia personalizada","search":"Busca en","pleaseInputNumber":"Introduce un número","mostValue":"Más valor","maxRating":"Clasificación máxima","notSelect":"No seleccionado","halfSelect":"Media selección","pleaseSelect":"Selecciona","title":"Título","content":"Contenido","componentNotFound":"El componente no existe","example":"Ejemplos","defaultMethodDesc":"Establecer el valor de la propiedad {nombre}","propertyUsage":"Puedes leer información relacionada con el componente accediendo a las propiedades del componente por su nombre en cualquier lugar donde puedas escribir JavaScript.","property":"Propiedades","propertyName":"Nombre de la propiedad","propertyType":"Tipo","propertyDesc":"Descripción","event":"Eventos","eventName":"Nombre del evento","eventDesc":"Descripción","mehtod":"Métodos","methodUsage":"Puedes interactuar con los componentes a través de sus métodos, y puedes llamarlos por su nombre en cualquier lugar donde puedas escribir JavaScript. O puedes llamarlos a través de la acción \"Componente de control\" de un evento.","methodName":"Nombre del método","methodDesc":"Descripción","showBorder":"Mostrar borde","haveTry":"Pruébalo tú mismo","settings":"Configurar","settingValues":"Valor de ajuste","defaultValue":"Valor por defecto","time":"Tiempo","date":"Fecha","noValue":"Ninguno","xAxisType":"Tipo de eje X","hAlignType":"Alineación horizontal","leftLeftAlign":"Alineación izquierda-izquierda","leftRightAlign":"Alineación izquierda-derecha","topLeftAlign":"Alineación superior izquierda","topRightAlign":"Alineación superior derecha","validation":"Validación","required":"Necesario","defaultStartDateValue":"Fecha de inicio por defecto","defaultEndDateValue":"Fecha de finalización por defecto","basicUsage":"Uso básico","basicDemoDescription":"Los siguientes ejemplos muestran el uso básico del componente.","noDefaultValue":"Sin valor por defecto","forbid":"Prohibido","placeholder":"Marcador de posición","pleaseInputPassword":"Introduce una contraseña","password":"Contraseña","textAlign":"Alineación del texto","length":"Longitud","top":"Arriba","pleaseInputName":"Introduce tu nombre","userName":"Nombre","fixed":"Fijo","responsive":"Respuesta","workCount":"Recuento de palabras","cascaderOptions":"Opciones de Cascader","pleaseSelectCity":"Selecciona una ciudad","advanced":"Avanzado","showClearIcon":"Mostrar icono Borrar","likedFruits":"Favoritos","option":"Opción","singleFileUpload":"Carga de un solo archivo","multiFileUpload":"Carga múltiple de archivos","folderUpload":"Cargar carpeta","multiFile":"Varios archivos","folder":"Carpeta","open":"Abre","favoriteFruits":"Frutas favoritas","pleaseSelectOneFruit":"Selecciona una fruta","notComplete":"No Completo","complete":"Completa","echart":"EChart","lineChart":"Gráfico lineal","basicLineChart":"Gráfico de líneas básico","lineChartType":"Tipo de gráfico de líneas","stackLineChart":"Línea apilada","areaLineChart":"Línea de área","scatterChart":"Gráfico de dispersión","scatterShape":"Forma de dispersión","scatterShapeCircle":"Círculo","scatterShapeRect":"Rectángulo","scatterShapeTri":"Triángulo","scatterShapeDiamond":"Diamante","scatterShapePin":"Chincheta","scatterShapeArrow":"Flecha","pieChart":"Gráfico circular","basicPieChart":"Gráfico circular básico","pieChatType":"Tipo de gráfico circular","pieChartTypeCircle":"Gráfico de donuts","pieChartTypeRose":"Gráfico de rosas","titleAlign":"Título Cargo","color":"Color","dashed":"Guiones","imADivider":"Soy una línea divisoria","tableSize":"Tamaño de la tabla","subMenuItem":"SubMenú {num}","menuItem":"Menú {num}","labelText":"Etiqueta","labelPosition":"Etiqueta - Posición","labelAlign":"Etiqueta - Alinear","optionsOptionType":"Método de configuración","styleBackgroundColor":"Color de fondo","styleBorderColor":"Color del borde","styleColor":"Color de fuente","selectionMode":"Modo de selección de filas","paginationSetting":"Configuración de la paginación","paginationShowSizeChanger":"Ayudar a los usuarios a modificar el número de entradas por página","paginationShowSizeChangerButton":"Mostrar botón de cambio de tamaño","paginationShowQuickJumper":"Mostrar saltador rápido","paginationHideOnSinglePage":"Ocultar cuando sólo hay una página","paginationPageSizeOptions":"Tamaño de página","chartConfigCompType":"Tipo de gráfico","xConfigType":"Tipo de eje X","loading":"Cargando","disabled":"Discapacitados","minLength":"Longitud mínima","maxLength":"Longitud máxima","showCount":"Mostrar recuento de palabras","autoHeight":"Altura","thousandsSeparator":"Separador de miles","precision":"Posiciones decimales","value":"Valor por defecto","formatter":"Formato","min":"Valor mínimo","max":"Valor máximo","step":"Tamaño del paso","start":"Hora de inicio","end":"Fin de los tiempos","allowHalf":"Permitir media selección","filetype":"Tipo de archivo","showUploadList":"Mostrar lista de cargas","uploadType":"Tipo de carga","allowClear":"Mostrar icono Borrar","minSize":"Tamaño mínimo del archivo","maxSize":"Tamaño máximo del archivo","maxFiles":"Número máximo de archivos cargados","format":"Formato","minDate":"Fecha mínima","maxDate":"Fecha máxima","minTime":"Tiempo mínimo","maxTime":"Tiempo máximo","text":"Texto","type":"Tipo","hideHeader":"Ocultar cabecera","hideBordered":"Ocultar borde","src":"URL de la imagen","showInfo":"Mostrar valor","mode":"Modo","onlyMenu":"Sólo Menú","horizontalAlignment":"Alineación horizontal","row":"Izquierda","column":"Arriba","leftAlign":"Alineación izquierda","rightAlign":"Alineación correcta","percent":"Porcentaje","fixedHeight":"Altura fija","auto":"Adaptativo","directory":"Carpeta","multiple":"Varios archivos","singleFile":"Archivo único","manual":"Manual","default":"Por defecto","small":"Pequeño","middle":"Medio","large":"Grande","single":"Individual","multi":"Múltiple","close":"Cerrar","ui":"Modo IU","line":"Gráfico lineal","scatter":"Gráfico de dispersión","pie":"Gráfico circular","basicLine":"Gráfico de líneas básico","stackedLine":"Gráfico de líneas apiladas","areaLine":"Área Mapa del área","basicPie":"Gráfico circular básico","doughnutPie":"Gráfico de donuts","rosePie":"Gráfico de rosas","category":"Categoría Eje","circle":"Círculo","rect":"Rectángulo","triangle":"Triángulo","diamond":"Diamante","pin":"Chincheta","arrow":"Flecha","left":"Izquierda","right":"A la derecha","center":"Centro","bottom":"Fondo","justify":"Justificar ambos extremos"},"playground":{"url":"https://app.lowcoder.cloud/playground/{compType}/1","data":"Estado actual de los datos","preview":"Vista previa","property":"Propiedades","console":"Consola Visual Script","executeMethods":"Ejecutar métodos","noMethods":"Sin métodos.","methodParams":"Parámetros del método","methodParamsHelp":"Parámetros del método de entrada utilizando JSON. Por ejemplo, puedes establecer los parámetros de setValue\\ con: [1] o 1"},"calendar":{"headerBtnBackground":"Botón Fondo","btnText":"Texto del botón","title":"Título","selectBackground":"Antecedentes seleccionados"},"componentDocExtra":{"table":"Documentación adicional para el componente Tabla"},"idSource":{"title":"Proveedores OAuth","form":"Envía un correo electrónico a","pay":"Premium","enable":"Activa","unEnable":"No activado","loginType":"Tipo de conexión","status":"Estado","desc":"Descripción","manual":"Agenda:","syncManual":"Sincronizar Agenda","syncManualSuccess":"Sincronización realizada","enableRegister":"Permitir la inscripción","saveBtn":"Guardar y Activar","save":"Guarda","none":"Ninguno","formPlaceholder":"Introduce {etiqueta}","formSelectPlaceholder":"Selecciona la {etiqueta}","saveSuccess":"Guardado correctamente","dangerLabel":"Zona de peligro","dangerTip":"Desactivar este proveedor de ID puede provocar que algunos usuarios no puedan iniciar sesión. Procede con precaución.","disable":"Desactiva","disableSuccess":"Desactivado correctamente","encryptedServer":"-------- Cifrado en el lado del servidor --------","disableTip":"Consejos","disableContent":"Desactivar este proveedor de ID puede provocar que algunos usuarios no puedan iniciar sesión. ¿Estás seguro de proceder?","manualTip":"","lockTip":"El Contenido está Bloqueado. Para realizar cambios, haz clic en el {icono} para desbloquearlo.","lockModalContent":"La modificación del campo \"Atributo ID\" puede tener repercusiones importantes en la identificación del usuario. Por favor, confirma que comprendes las implicaciones de este cambio antes de proceder.","payUserTag":"Premium"},"slotControl":{"configSlotView":"Configurar vista de ranura"},"jsonLottie":{"lottieJson":"Lottie JSON","speed":"Velocidad","width":"Anchura","height":"Altura","backgroundColor":"Color de fondo","animationStart":"Inicio de la animación","valueDesc":"Datos JSON actuales","loop":"Bucle","auto":"Auto","onHover":"Al pasar por encima","singlePlay":"Juego individual","endlessLoop":"Bucle sin fin","keepLastFrame":"Mantener visualizado el último fotograma"},"timeLine":{"titleColor":"Título Color","subTitleColor":"Color del subtítulo","lableColor":"Color de la etiqueta","value":"Datos cronológicos","mode":"Orden de visualización","left":"Contenido Correcto","right":"Contenido Izquierda","alternate":"Orden alternativo del contenido","modeTooltip":"Configura el contenido para que aparezca a izquierda/derecha o alternativamente en ambos lados de la línea de tiempo","reverse":"Eventos más recientes primero","pending":"Texto de nodo pendiente","pendingDescription":"Si se establece, se mostrará un último nodo con el texto y un indicador de espera.","defaultPending":"Mejora continua","clickTitleEvent":"Haz clic en el título Evento","clickTitleEventDesc":"Haz clic en el título Evento","Introduction":"Introducción Claves","helpTitle":"Título del cronograma (Obligatorio)","helpsubTitle":"Subtítulo del cronograma","helpLabel":"Etiqueta de la línea de tiempo, utilizada para mostrar las fechas","helpColor":"Indica el color del nodo de la línea de tiempo","helpDot":"Representación de los nodos de la línea de tiempo como iconos de diseño Ant","helpTitleColor":"Controlar individualmente el color del título del nodo","helpSubTitleColor":"Controlar Individualmente el Color del Subtítulo del Nodo","helpLableColor":"Controlar Individualmente el Color del Icono del Nodo","valueDesc":"Datos de la cronología","clickedObjectDesc":"Datos del elemento pulsado","clickedIndexDesc":"Índice de elementos pulsados"},"comment":{"value":"Datos de la lista de comentarios","showSendButton":"Permitir comentarios","title":"Título","titledDefaultValue":"%d Comentario en total","placeholder":"Mayús + Intro para comentar; introduce @ o # para entrada rápida","placeholderDec":"Marcador de posición","buttonTextDec":"Título del botón","buttonText":"Comentario","mentionList":"Datos de la Lista de Menciones","mentionListDec":"Palabras clave de mención clave; Datos de la lista de mención de valor","userInfo":"Información del usuario","dateErr":"Error de fecha","commentList":"Lista de comentarios","deletedItem":"Elemento eliminado","submitedItem":"Artículo enviado","deleteAble":"Mostrar botón Eliminar","Introduction":"Introducción Claves","helpUser":"Información del usuario (Obligatorio)","helpname":"Nombre de usuario (Obligatorio)","helpavatar":"Avatar URL (Alta prioridad)","helpdisplayName":"Nombre para mostrar (prioridad baja)","helpvalue":"Contenido de los comentarios","helpcreatedAt":"Fecha de creación"},"mention":{"mentionList":"Datos de la Lista de Menciones"},"autoComplete":{"value":"Auto Complete Value","checkedValueFrom":"Valor comprobado Desde","ignoreCase":"Buscar Ignorar Caso","searchLabelOnly":"Buscar sólo etiqueta","searchFirstPY":"Buscar First Pinyin","searchCompletePY":"Buscar Pinyin completo","searchText":"Buscar texto","SectionDataName":"Autocompletar datos","valueInItems":"Valor en artículos","type":"Tipo","antDesign":"AntDesign","normal":"Normal","selectKey":"Clave","selectLable":"Etiqueta","ComponentType":"Tipo de componente","colorIcon":"Azul","grewIcon":"Gris","noneIcon":"Ninguno","small":"Pequeño","large":"Grande","componentSize":"Tamaño del componente","Introduction":"Introducción Claves","helpLabel":"Etiqueta","helpValue":"Valor"},"responsiveLayout":{"column":"Columnas","atLeastOneColumnError":"El diseño adaptable mantiene al menos una columna","columnsPerRow":"Columnas por fila","columnsSpacing":"Espacio entre columnas (px)","horizontal":"Horizontal","vertical":"Vertical","mobile":"Móvil","tablet":"Tableta","desktop":"Escritorio","rowStyle":"Estilo Fila","columnStyle":"Estilo columna","minWidth":"Mín. Anchura","rowBreak":"Rotura de fila","matchColumnsHeight":"Igualar altura de columnas","rowLayout":"Disposición de filas","columnsLayout":"Disposición de las columnas"},"navLayout":{"mode":"Modo","modeInline":"En línea","modeVertical":"Vertical","width":"Anchura","widthTooltip":"Píxel o Porcentaje, por ejemplo 520, 60%.","navStyle":"Estilo de menú","navItemStyle":"Estilo del elemento de menú"}} \ No newline at end of file +{"productName":"Lowcoder","productDesc":"Crea aplicaciones de software para tu empresa y tus clientes con una experiencia mínima en codificación. Lowcoder es una excelente alternativa a Retool, Appsmith y Tooljet.","notSupportedBrowser":"Tu navegador actual puede tener problemas de compatibilidad. Para una experiencia de usuario óptima, utiliza la última versión de Chrome.","create":"Crea","move":"Muévete","addItem":"Añade","newItem":"Nuevo","copy":"Copia","rename":"Cambia el nombre de","delete":"Borra","deletePermanently":"Borrar permanentemente","remove":"Elimina","recover":"Recupera","edit":"Edita","view":"Ver","value":"Valor","data":"Datos","information":"Información","success":"Éxito","warning":"Advertencia","error":"Error","reference":"Referencia","text":"Texto","label":"Etiqueta","color":"Color","form":"Formulario","menu":"Menú","menuItem":"Elemento del menú","ok":"OK","cancel":"Cancelar","finish":"Acabado","reset":"Restablece","icon":"Icono","code":"Código","title":"Título","emptyContent":"Contenido vacío","more":"Más","search":"Busca en","back":"Volver","accessControl":"Control de acceso","copySuccess":"Copiado correctamente","copyError":"Error de copia","api":{"publishSuccess":"Publicado con éxito","recoverFailed":"Recuperación fallida","needUpdate":"Tu versión actual está obsoleta. Por favor, actualízala a la última versión."},"codeEditor":{"notSupportAutoFormat":"El editor de código actual no admite el autoformateo.","fold":"Pliega"},"exportMethod":{"setDesc":"Establecer propiedad: {propiedad}","clearDesc":"Borrar propiedad: {propiedad}","resetDesc":"Restablecer propiedad: {propiedad} al valor por defecto"},"method":{"focus":"Focalizar","focusOptions":"Opciones de enfoque. Ver HTMLElement.focus()","blur":"Eliminar Enfoque","click":"Haz clic en","select":"Seleccionar todo el texto","setSelectionRange":"Establecer las posiciones inicial y final de la selección de texto","selectionStart":"Índice basado en 0 del primer carácter seleccionado","selectionEnd":"Índice basado en 0 del carácter después del último carácter seleccionado","setRangeText":"Reemplazar rango de texto","replacement":"Cadena a insertar","replaceStart":"Índice basado en 0 del primer carácter a sustituir","replaceEnd":"Índice basado en 0 del carácter después del último carácter a sustituir"},"errorBoundary":{"encounterError":"Error en la carga del componente. Comprueba tu configuración.","clickToReload":"Haz clic para recargar","errorMsg":"Error: "},"imgUpload":{"notSupportError":"Sólo admite tipos de imagen {tipos}","exceedSizeError":"El tamaño de la imagen no debe superar el {tamaño}."},"gridCompOperator":{"notSupport":"No admitido","selectAtLeastOneComponent":"Selecciona al menos un componente","selectCompFirst":"Seleccionar componentes antes de copiar","noContainerSelected":"[Bug] No se ha seleccionado ningún contenedor","deleteCompsSuccess":"Eliminado correctamente. Pulsa {Tecla Deshacer} para deshacer.","deleteCompsTitle":"Eliminar componentes","deleteCompsBody":"¿Estás seguro de que quieres borrar {compNum} componentes seleccionados?","cutCompsSuccess":"Corta con éxito. Pulsa {pegarTecla} para pegar, o {deshacerTecla} para deshacer."},"leftPanel":{"queries":"Consultas de datos en tu app","globals":"Variables de datos globales","propTipsArr":"{num} Elementos","propTips":"{num} Teclas","propTipArr":"{num} Artículo","propTip":"{num} Tecla","stateTab":"Estado","settingsTab":"Ajustes","toolbarTitle":"Individualización","toolbarPreload":"Guiones y estilos","components":"Componentes activos","modals":"Modales in-App","expandTip":"Haz clic para ampliar los datos del {componente}","collapseTip":"Haz clic para contraer los datos del {componente}"},"bottomPanel":{"title":"Consultas de datos","run":"Ejecuta","noSelectedQuery":"No se ha seleccionado ninguna consulta","metaData":"Metadatos de la fuente de datos","noMetadata":"No hay metadatos disponibles","metaSearchPlaceholder":"Buscar metadatos","allData":"Todas las mesas"},"rightPanel":{"propertyTab":"Propiedades","noSelectedComps":"No hay Componentes seleccionados. Haz clic en un Componente para ver sus Propiedades.","createTab":"Inserta","searchPlaceHolder":"Buscar componentes o módulos","uiComponentTab":"Componentes","extensionTab":"Extensiones","modulesTab":"Módulos","moduleListTitle":"Módulos","pluginListTitle":"Plugins","emptyModules":"Los módulos son Mikro-Apps reutilizables. Puedes incrustarlos en tu App.","searchNotFound":"¿No encuentras el componente adecuado? Envía un problema","emptyPlugins":"No se han añadido plugins","contactUs":"Contacta con nosotros","issueHere":"aquí."},"prop":{"expand":"Amplía","columns":"Columnas","videokey":"Llave de vídeo","rowSelection":"Selección de filas","toolbar":"Barra de herramientas","pagination":"Paginación","logo":"Logotipo","style":"Estilo","inputs":"Entradas","meta":"Metadatos","data":"Datos","hide":"Ocultar","loading":"Cargando","disabled":"Discapacitados","placeholder":"Marcador de posición","showClear":"Mostrar botón Borrar","showSearch":"Puedes buscar en","defaultValue":"Valor por defecto","required":"Campo obligatorio","readOnly":"Sólo lectura","readOnlyTooltip":"Los componentes de sólo lectura parecen normales, pero no se pueden modificar.","minimum":"Mínimo","maximum":"Máximo","regex":"Regex","minLength":"Longitud mínima","maxLength":"Longitud máxima","height":"Altura","width":"Anchura","selectApp":"Seleccionar aplicación","showCount":"Mostrar recuento","textType":"Tipo de texto","customRule":"Regla personalizada","customRuleTooltip":"Una cadena no vacía indica un error; vacía o nula significa que la validación se ha superado. Ejemplo: ","manual":"Manual","map":"Mapa","json":"JSON","use12Hours":"Utiliza el formato de 12 horas","hourStep":"Hora Paso","minuteStep":"Paso del minuto","secondStep":"Segundo paso","minDate":"Fecha mínima","maxDate":"Fecha máxima","minTime":"Tiempo mínimo","maxTime":"Tiempo máximo","type":"Tipo","showLabel":"Mostrar etiqueta","showHeader":"Mostrar cabecera","showBody":"Mostrar cuerpo","showFooter":"Mostrar pie de página","maskClosable":"Pulsa Fuera para Cerrar","showMask":"Mostrar máscara"},"autoHeightProp":{"auto":"Auto","fixed":"Fijo"},"labelProp":{"text":"Etiqueta","tooltip":"Información sobre herramientas","position":"Posición","left":"Izquierda","top":"Arriba","align":"Alineación","width":"Anchura","widthTooltip":"La anchura de la etiqueta admite porcentajes (%) y píxeles (px)."},"eventHandler":{"eventHandlers":"Manejadores de eventos","emptyEventHandlers":"Sin manejadores de eventos","incomplete":"Selección incompleta","inlineEventTitle":"En {eventName}","event":"Evento","action":"Acción","noSelect":"Sin selección","runQuery":"Ejecutar una consulta de datos","selectQuery":"Seleccionar consulta de datos","controlComp":"Controlar un componente","runScript":"Ejecutar JavaScript","runScriptPlaceHolder":"Escribe aquí el código","component":"Componente","method":"Método","setTempState":"Establece un valor de Estado Temporal","state":"Estado","triggerModuleEvent":"Activar un evento de módulo","moduleEvent":"Módulo Evento","goToApp":"Ir a otra App","queryParams":"Parámetros de consulta","hashParams":"Parámetros Hash","showNotification":"Mostrar una notificación","text":"Texto","level":"Nivel","duration":"Duración","notifyDurationTooltip":"La unidad de tiempo puede ser \\'s\\' (segundo, por defecto) o \\'ms\\' (milisegundo). La duración máxima es {max} segundos","goToURL":"Abrir una URL","openInNewTab":"Abrir en pestaña nueva","copyToClipboard":"Copiar un valor al Portapapeles","copyToClipboardValue":"Valor","export":"Exportar datos","exportNoFileType":"Sin selección (Opcional)","fileName":"Nombre del archivo","fileNameTooltip":"Incluye la extensión para especificar el tipo de archivo, por ejemplo: \\'imagen.png'","fileType":"Tipo de archivo","condition":"Corre sólo cuando...","conditionTooltip":"Ejecuta el controlador de eventos sólo cuando esta condición se evalúe como \"verdadero\".","debounce":"Rebote para","throttle":"Acelerador para","slowdownTooltip":"Utiliza debounce o throttle para controlar la frecuencia de los disparos de acción. La unidad de tiempo puede ser \\'ms\\' (milisegundo, por defecto) o \\'s\\' (segundo).","notHandledError":"No manipulado","currentApp":"Actual"},"event":{"submit":"Envía","submitDesc":"Activadores al enviar","change":"Cambia","changeDesc":"Activadores de cambios de valor","focus":"Enfoque","focusDesc":"Activadores en Foco","blur":"Desenfocar","blurDesc":"Activadores del desenfoque","click":"Haz clic en","clickDesc":"Activadores al hacer clic","close":"Cerrar","closeDesc":"Desencadenantes al cerrar","parse":"Analiza","parseDesc":"Disparadores en Parse","success":"Éxito","successDesc":"Activadores del éxito","delete":"Borra","deleteDesc":"Activadores al borrar","mention":"Menciona","mentionDesc":"Activadores de la mención"},"themeDetail":{"primary":"Color de la marca","primaryDesc":"Color primario por defecto utilizado por la mayoría de los componentes","textDark":"Color de texto oscuro","textDarkDesc":"Se utiliza cuando el color de fondo es claro","textLight":"Color de texto claro","textLightDesc":"Se utiliza cuando el color de fondo es oscuro","canvas":"Color del lienzo","canvasDesc":"Color de fondo predeterminado de la aplicación","primarySurface":"Color del envase","primarySurfaceDesc":"Color de fondo por defecto para componentes como las tablas","borderRadius":"Radio del borde","borderRadiusDesc":"Radio del borde por defecto utilizado por la mayoría de los componentes","chart":"Estilo gráfico","chartDesc":"Entrada para Echarts","echartsJson":"Tema JSON","margin":"Margen","marginDesc":"Margen por defecto utilizado normalmente para la mayoría de los componentes","padding":"Acolchado","paddingDesc":"Relleno por defecto utilizado normalmente para la mayoría de los componentes","containerHeaderPadding":"Relleno de cabecera","containerheaderpaddingDesc":"Relleno de cabecera por defecto utilizado normalmente para la mayoría de los componentes","gridColumns":"Columnas de cuadrícula","gridColumnsDesc":"Número predeterminado de columnas utilizado normalmente para la mayoría de los contenedores"},"style":{"resetTooltip":"Restablecer estilos. Borra el campo de entrada para restablecer un estilo individual.","textColor":"Color del texto","contrastText":"Contraste Color del texto","generated":"Generado","customize":"Personaliza","staticText":"Texto estático","accent":"Acento","validate":"Mensaje de validación","border":"Color del borde","borderRadius":"Radio del borde","borderWidth":"Anchura del borde","background":"Antecedentes","headerBackground":"Fondo de la cabecera","footerBackground":"Fondo de pie de página","fill":"Rellena","track":"Pista","links":"Enlaces","thumb":"Pulgar","thumbBorder":"Borde del pulgar","checked":"Comprobado","unchecked":"Sin marcar","handle":"Asa","tags":"Etiquetas","tagsText":"Etiquetas Texto","multiIcon":"Icono multiselección","tabText":"Texto de la pestaña","tabAccent":"Acento de pestaña","checkedBackground":"Antecedentes comprobados","uncheckedBackground":"Fondo sin marcar","uncheckedBorder":"Frontera sin marcar","indicatorBackground":"Indicador Antecedentes","tableCellText":"Texto celular","selectedRowBackground":"Fondo de fila seleccionado","hoverRowBackground":"Fondo de la fila Hover","alternateRowBackground":"Fondo de fila alternativo","tableHeaderBackground":"Fondo de la cabecera","tableHeaderText":"Texto de cabecera","toolbarBackground":"Fondo de la barra de herramientas","toolbarText":"Texto de la barra de herramientas","pen":"Bolígrafo","footerIcon":"Icono de pie de página","tips":"Consejos","margin":"Margen","padding":"Acolchado","marginLeft":"Margen izquierdo","marginRight":"Margen derecho","marginTop":"Margen superior","marginBottom":"Margen inferior","containerHeaderPadding":"Relleno de cabecera","containerFooterPadding":"Relleno de pie de página","containerBodyPadding":"Acolchado corporal","minWidth":"Anchura mínima","aspectRatio":"Relación de aspecto","textSize":"Tamaño del texto"},"export":{"hiddenDesc":"Si es verdadero, el componente se oculta","disabledDesc":"Si es verdadero, el componente está desactivado y no es interactivo","visibleDesc":"Si es verdadero, el componente es visible","inputValueDesc":"Valor actual de la entrada","invalidDesc":"Indica si el valor no es válido","placeholderDesc":"Texto de marcador de posición cuando no se establece ningún valor","requiredDesc":"Si es verdadero, se requiere un valor válido","submitDesc":"Enviar formulario","richTextEditorValueDesc":"Valor actual del Editor","richTextEditorReadOnlyDesc":"Si es verdadero, el Editor es de sólo lectura","richTextEditorHideToolBarDesc":"Si es verdadero, la barra de herramientas se oculta","jsonEditorDesc":"Datos JSON actuales","sliderValueDesc":"Valor seleccionado actualmente","sliderMaxValueDesc":"Valor máximo del deslizador","sliderMinValueDesc":"Valor mínimo de la corredera","sliderStartDesc":"Valor del punto de partida seleccionado","sliderEndDesc":"Valor del punto final seleccionado","ratingValueDesc":"Clasificación seleccionada actualmente","ratingMaxDesc":"Valor nominal máximo","datePickerValueDesc":"Fecha seleccionada actualmente","datePickerFormattedValueDesc":"Fecha seleccionada formateada","datePickerTimestampDesc":"Marca de tiempo de la fecha seleccionada","dateRangeStartDesc":"Fecha de inicio del rango","dateRangeEndDesc":"Fecha final del intervalo","dateRangeStartTimestampDesc":"Marca de tiempo de la fecha de inicio","dateRangeEndTimestampDesc":"Marca de tiempo de la fecha de finalización","dateRangeFormattedValueDesc":"Rango de fechas formateado","dateRangeFormattedStartValueDesc":"Fecha de inicio formateada","dateRangeFormattedEndValueDesc":"Fecha final formateada","timePickerValueDesc":"Hora seleccionada actualmente","timePickerFormattedValueDesc":"Hora seleccionada formateada","timeRangeStartDesc":"Hora de inicio del alcance","timeRangeEndDesc":"Hora de finalización del alcance","timeRangeFormattedValueDesc":"Rango de tiempo formateado","timeRangeFormattedStartValueDesc":"Hora de inicio formateada","timeRangeFormattedEndValueDesc":"Hora final formateada"},"validationDesc":{"email":"Introduce una dirección de correo electrónico válida","url":"Por favor, introduce una URL válida","regex":"Concuerda con el patrón especificado","maxLength":"Demasiados caracteres, actual: {longitud}, máximo: {longitudmáxima}","minLength":"No hay suficientes caracteres, actual: {longitud}, mínimo: {longitud mínima}","maxValue":"El valor supera el máximo, actual: {valor}, máximo: {máximo}","minValue":"Valor por debajo del mínimo, actual: {valor}, mínimo: {mín}","maxTime":"El tiempo supera el máximo, actual: {hora}, máximo: {tiempomáx}","minTime":"Tiempo por debajo del mínimo, actual: {hora}, mínimo: {minTime}","maxDate":"La fecha supera el máximo, actual: {fecha}, máximo: {fechaMáx}","minDate":"Fecha por debajo del mínimo, actual: {fecha}, mínimo: {fechaMín}"},"query":{"noQueries":"No hay Consultas de Datos disponibles.","queryTutorialButton":"Ver documentos {valor}","datasource":"Tus fuentes de datos","newDatasource":"Nueva fuente de datos","generalTab":"General","notificationTab":"Notificación","advancedTab":"Avanzado","showFailNotification":"Mostrar notificación en caso de fallo","failCondition":"Condiciones de fallo","failConditionTooltip1":"Personaliza las condiciones de fallo y las notificaciones correspondientes.","failConditionTooltip2":"Si alguna condición devuelve verdadero, la consulta se marca como fallida y activa la notificación correspondiente.","showSuccessNotification":"Mostrar notificación de éxito","successMessageLabel":"Mensaje de éxito","successMessage":"Corre con éxito","notifyDuration":"Duración","notifyDurationTooltip":"Duración de la notificación. La unidad de tiempo puede ser \\'s\\' (segundo, por defecto) o \\'ms\\' (milisegundo). El valor por defecto es {default}s. El máximo es {max}s.","successMessageWithName":"{nombre} ejecutado correctamente","failMessageWithName":"Ha fallado la ejecución de {nombre}: {resultado}","showConfirmationModal":"Mostrar Modal de Confirmación Antes de Ejecutar","confirmationMessageLabel":"Mensaje de confirmación","confirmationMessage":"¿Estás seguro de que quieres ejecutar esta Consulta de Datos?","newQuery":"Nueva consulta de datos","newFolder":"Carpeta nueva","recentlyUsed":"Usado recientemente","folder":"Carpeta","folderNotEmpty":"La carpeta no está vacía","dataResponder":"Respondedor de datos","tempState":"Estado Temporal","transformer":"Transformador","quickRestAPI":"Consulta REST","quickStreamAPI":"Consulta de flujos","quickGraphql":"Consulta GraphQL","lowcoderAPI":"API Lowcoder","executeJSCode":"Ejecutar código JavaScript","importFromQueryLibrary":"Importar desde la biblioteca de consultas","importFromFile":"Importar desde archivo","triggerType":"Se activa cuando...","triggerTypeAuto":"Cambio de entradas o al cargar la página","triggerTypePageLoad":"Cuando se carga la Aplicación (Página)","triggerTypeManual":"Sólo cuando lo activas manualmente","chooseDataSource":"Elegir fuente de datos","method":"Método","updateExceptionDataSourceTitle":"Actualizar fuente de datos que falla","updateExceptionDataSourceContent":"Actualiza la siguiente consulta con la misma fuente de datos que falla:","update":"Actualiza","disablePreparedStatement":"Desactivar Declaraciones Preparadas","disablePreparedStatementTooltip":"Desactivar las sentencias preparadas puede generar SQL dinámico, pero aumenta el riesgo de inyección SQL","timeout":"Tiempo de espera tras","timeoutTooltip":"Unidad por defecto: ms. Unidades de entrada admitidas: ms, s. Valor por defecto: {defaultSeconds} segundos. Valor máximo: {maxSeconds} segundos. Por ejemplo, 300 (es decir, 300ms), 800ms, 5s.","periodic":"Ejecuta periódicamente esta consulta de datos","periodicTime":"Periodo","periodicTimeTooltip":"Periodo entre ejecuciones sucesivas. Unidad por defecto: ms. Unidades de entrada admitidas: ms, s. Valor mínimo: 100ms. La ejecución periódica se desactiva para valores inferiores a 100ms. Por ejemplo, 300 (es decir, 300ms), 800ms, 5s.","cancelPrevious":"Ignorar los resultados de ejecuciones anteriores no completadas","cancelPreviousTooltip":"Si se desencadena una nueva ejecución, se ignorará el resultado de las ejecuciones anteriores no completadas si no se completaron, y estas ejecuciones ignoradas no desencadenarán la lista de eventos de la consulta.","dataSourceStatusError":"Si se desencadena una nueva ejecución, se ignorará el resultado de las ejecuciones anteriores no completadas, y las ejecuciones ignoradas no desencadenarán la lista de eventos de la consulta.","success":"Éxito","fail":"Fallo","successDesc":"Se activa cuando la ejecución tiene éxito","failDesc":"Se activa cuando falla la ejecución","fixedDelayError":"Consulta no ejecutada","execSuccess":"Corre con éxito","execFail":"Error de ejecución","execIgnored":"Los resultados de esta consulta fueron ignorados","deleteSuccessMessage":"Eliminado con éxito. Puedes utilizar {undoKey} para Deshacer","dataExportDesc":"Datos obtenidos por la consulta actual","codeExportDesc":"Código de estado de la consulta actual","successExportDesc":"Si la consulta actual se ha ejecutado correctamente","messageExportDesc":"Información devuelta por la consulta actual","extraExportDesc":"Otros datos de la consulta actual","isFetchingExportDesc":"¿Es la consulta actual de la petición?","runTimeExportDesc":"Tiempo de ejecución de la consulta actual (ms)","latestEndTimeExportDesc":"Último tiempo de ejecución","triggerTypeExportDesc":"Tipo de gatillo","chooseResource":"Elige un Recurso","createDataSource":"Crear una nueva fuente de datos","editDataSource":"Edita","datasourceName":"Nombre","datasourceNameRuleMessage":"Introduce un nombre de fuente de datos","generalSetting":"Configuración general","advancedSetting":"Ajustes avanzados","port":"Puerto","portRequiredMessage":"Introduce un puerto","portErrorMessage":"Introduce un puerto correcto","connectionType":"Tipo de conexión","regular":"Regular","host":"Anfitrión","hostRequiredMessage":"Introduce un nombre de dominio de host o una dirección IP","userName":"Nombre de usuario","password":"Contraseña","encryptedServer":"-------- Cifrado en el lado del servidor --------","uriRequiredMessage":"Introduce un URI","urlRequiredMessage":"Introduce una URL","uriErrorMessage":"Por favor, introduce un URI correcto","urlErrorMessage":"Introduce una URL correcta","httpRequiredMessage":"Introduce http:// o https://","databaseName":"Nombre de la base de datos","databaseNameRequiredMessage":"Introduce el nombre de la base de datos","useSSL":"Utiliza SSL","userNameRequiredMessage":"Introduce tu nombre","passwordRequiredMessage":"Introduce tu contraseña","authentication":"Autenticación","authenticationType":"Tipo de autenticación","sslCertVerificationType":"Verificación de certificados SSL","sslCertVerificationTypeDefault":"Verificar CA Cert","sslCertVerificationTypeSelf":"Verificar certificado autofirmado","sslCertVerificationTypeDisabled":"Discapacitados","selfSignedCert":"Certificado autofirmado","selfSignedCertRequireMsg":"Introduce tu certificado","enableTurnOffPreparedStatement":"Activar la alternancia de sentencias preparadas para consultas","enableTurnOffPreparedStatementTooltip":"Puedes activar o desactivar las sentencias preparadas en la pestaña Avanzado de la consulta","serviceName":"Nombre del servicio","serviceNameRequiredMessage":"Introduce el nombre de tu servicio","useSID":"Utiliza el SID","connectSuccessfully":"Conexión correcta","saveSuccessfully":"Guardado correctamente","database":"Base de datos","cloudHosting":"Lowcoder alojado en la nube no puede acceder a los servicios locales utilizando 127.0.0.1 o localhost. Intenta conectarte a fuentes de datos de la red pública o utiliza un proxy inverso para los servicios privados.","notCloudHosting":"Para el despliegue alojado en Docker, Lowcoder utiliza redes puente, por lo que 127.0.0.1 y localhost no son válidos para las direcciones de host. Para acceder a las fuentes de datos de la máquina local, consulta","howToAccessHostDocLink":"Cómo acceder a la API/DB del host","returnList":"Devuelve","chooseDatasourceType":"Elige el tipo de fuente de datos","viewDocuments":"Ver documentos","testConnection":"Conexión de prueba","save":"Guarda","whitelist":"Lista de permisos","whitelistTooltip":"Añade las direcciones IP de Lowcoder\\ a tu lista de fuentes de datos permitidas según sea necesario.","address":"Dirección: ","nameExists":"El nombre {nombre} ya existe","jsQueryDocLink":"Acerca de la consulta JavaScript","dynamicDataSourceConfigLoadingText":"Cargando configuración extra de fuente de datos...","dynamicDataSourceConfigErrText":"No se ha podido cargar la configuración adicional de la fuente de datos.","retry":"Reintentar"},"sqlQuery":{"keyValuePairs":"Pares clave-valor","object":"Objeto","allowMultiModify":"Permitir la modificación de varias filas","allowMultiModifyTooltip":"Si se selecciona, se operan todas las filas que cumplan las condiciones. En caso contrario, sólo se operará sobre la primera fila que cumpla las condiciones.","array":"Matriz","insertList":"Insertar lista","insertListTooltip":"Valores insertados cuando no existen","filterRule":"Regla de filtrado","updateList":"Actualizar lista","updateListTooltip":"Los valores actualizados tal como existen pueden ser anulados por los mismos valores de la lista de inserción","sqlMode":"Modo SQL","guiMode":"Modo GUI","operation":"Operación","insert":"Inserta","upsert":"Insertar, pero Actualizar si hay conflicto","update":"Actualiza","delete":"Borra","bulkInsert":"Inserción a granel","bulkUpdate":"Actualización masiva","table":"Tabla","primaryKeyColumn":"Columna de clave primaria"},"EsQuery":{"rawCommand":"Comando en bruto","queryTutorialButton":"Ver documentos de la API de Elasticsearch","request":"Solicita"},"googleSheets":{"rowIndex":"Índice de filas","spreadsheetId":"ID de la hoja de cálculo","sheetName":"Nombre de la hoja","readData":"Leer datos","appendData":"Añadir fila","updateData":"Actualizar Fila","deleteData":"Borrar fila","clearData":"Fila clara","serviceAccountRequireMessage":"Introduce tu cuenta de servicio","ASC":"ASC","DESC":"DESC","sort":"Clasificar","sortPlaceholder":"Nombre"},"queryLibrary":{"export":"Exportar a JSON","noInput":"La consulta actual no tiene entrada","inputName":"Nombre","inputDesc":"Descripción","emptyInputs":"Sin entradas","clickToAdd":"Añade","chooseQuery":"Elige Consulta","viewQuery":"Ver consulta","chooseVersion":"Elegir versión","latest":"Lo último en","publish":"Publica","historyVersion":"Historia Versión","deleteQueryLabel":"Borrar consulta","deleteQueryContent":"No se puede recuperar la consulta después de borrarla. ¿Borrar la consulta?","run":"Ejecuta","readOnly":"Sólo lectura","exit":"Salir","recoverAppSnapshotContent":"Restaurar la consulta actual a la versión {versión}","searchPlaceholder":"Consulta de búsqueda","allQuery":"Todas las consultas","deleteQueryTitle":"Borrar consulta","unnamed":"Sin nombre","publishNewVersion":"Publicar nueva versión","publishSuccess":"Publicado con éxito","version":"Versión","desc":"Descripción"},"snowflake":{"accountIdentifierTooltip":"Consulta ","extParamsTooltip":"Configurar parámetros de conexión adicionales"},"lowcoderQuery":{"queryOrgUsers":"Consultar usuarios del espacio de trabajo"},"redisQuery":{"rawCommand":"Comando en bruto","command":"Mando","queryTutorial":"Ver documentos sobre los comandos de Redis"},"httpQuery":{"bodyFormDataTooltip":"Si se selecciona {tipo}, el formato del valor debe ser {objeto}. Ejemplo: {ejemplo}","text":"Texto","file":"Archivo","extraBodyTooltip":"Los valores clave del cuerpo extra se añadirán al cuerpo con tipos de datos JSON o de formulario","forwardCookies":"Adelante Cookies","forwardAllCookies":"Reenviar todas las cookies"},"smtpQuery":{"attachment":"Adjunto","attachmentTooltip":"Puede utilizarse con el componente de carga de archivos, los datos deben convertirse a: ","MIMETypeUrl":"https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types","sender":"Remitente","recipient":"Destinatario","carbonCopy":"Copia al carbón","blindCarbonCopy":"Copia al carbón ciega","subject":"Asunto","content":"Contenido","contentTooltip":"Admite la introducción de texto o HTML"},"uiCompCategory":{"dashboards":"Cuadros de mando e informes","layout":"Diseño y navegación","forms":"Recogida de datos y formularios","collaboration":"Reunión y colaboración","projectmanagement":"Gestión de proyectos","scheduling":"Calendario y programación","documents":"Gestión de documentos y archivos","itemHandling":"Tramitación de artículos y firmas","multimedia":"Multimedia y animación","integration":"Integración y ampliación"},"uiComp":{"autoCompleteCompName":"Auto Completo","autoCompleteCompDesc":"Un campo de entrada que proporciona sugerencias mientras escribes, mejorando la experiencia del usuario y la precisión.","autoCompleteCompKeywords":"sugerencias, autocompletar, escribir, entrada","inputCompName":"Entrada","inputCompDesc":"Un campo de entrada de texto básico que permite a los usuarios introducir y editar texto.","inputCompKeywords":"texto, entrada, campo, editar","textAreaCompName":"Área de texto","textAreaCompDesc":"Una entrada de texto de varias líneas para contenidos de forma más larga, como comentarios o descripciones.","textAreaCompKeywords":"multilínea, área de texto, entrada, texto","passwordCompName":"Contraseña","passwordCompDesc":"Un campo seguro para introducir la contraseña, enmascarando los caracteres para mayor privacidad.","passwordCompKeywords":"contraseña, seguridad, entrada, oculto","richTextEditorCompName":"Editor de texto enriquecido","richTextEditorCompDesc":"Un editor de texto avanzado que admite numerosas opciones de formato, como negrita, cursiva y listas.","richTextEditorCompKeywords":"editor, texto, formato, contenido enriquecido","numberInputCompName":"Número Entrada","numberInputCompDesc":"Un campo específico para la introducción de datos numéricos, con controles para aumentar y disminuir los valores.","numberInputCompKeywords":"número, entrada, incremento, decremento","sliderCompName":"Deslizador","sliderCompDesc":"Componente gráfico deslizante para seleccionar un valor o rango dentro de una escala definida.","sliderCompKeywords":"deslizador, rango, entrada, gráfico","rangeSliderCompName":"Corredera de alcance","rangeSliderCompDesc":"Un deslizador de doble asa para seleccionar un rango de valores, útil para filtrar o establecer límites.","rangeSliderCompKeywords":"gama, deslizador, doble asa, filtro","ratingCompName":"Clasificación","ratingCompDesc":"Un componente para capturar las valoraciones de los usuarios, mostradas como estrellas.","ratingCompKeywords":"valoración, estrellas, opiniones, aportaciones","switchCompName":"Interruptor","switchCompDesc":"Un interruptor basculante para decisiones del tipo encendido/apagado o sí/no.","switchCompKeywords":"conmutador, interruptor, encendido/apagado, control","selectCompName":"Selecciona","selectCompDesc":"Un menú desplegable para seleccionar entre una lista de opciones.","selectCompKeywords":"desplegable, seleccionar, opciones, menú","multiSelectCompName":"Multiselector","multiSelectCompDesc":"Un componente que permite seleccionar varios elementos de una lista desplegable.","multiSelectCompKeywords":"multiselección, múltiple, desplegable, opciones","cascaderCompName":"Cascader","cascaderCompDesc":"Un desplegable de varios niveles para la selección jerárquica de datos, como la selección de una ubicación.","cascaderCompKeywords":"cascada, jerárquico, desplegable, niveles","checkboxCompName":"Casilla de verificación","checkboxCompDesc":"Una casilla de verificación estándar para opciones que se pueden seleccionar o deseleccionar.","checkboxCompKeywords":"casilla, opciones, seleccionar, alternar","radioCompName":"Radio","radioCompDesc":"Botones de radio para seleccionar una opción de un conjunto, donde sólo se permite una elección.","radioCompKeywords":"radio, botones, seleccionar, elección única","segmentedControlCompName":"Control segmentado","segmentedControlCompDesc":"Un control con opciones segmentadas para alternar rápidamente entre varias opciones.","segmentedControlCompKeywords":"segmentado, control, conmutar, opciones","fileUploadCompName":"Carga de archivos","fileUploadCompDesc":"Un componente para subir archivos, con soporte para arrastrar y soltar y selección de archivos.","fileUploadCompKeywords":"archivo, subir, arrastrar y soltar, seleccionar","dateCompName":"Fecha","dateCompDesc":"Un componente selector de fechas para seleccionar fechas de una interfaz de calendario.","dateCompKeywords":"fecha, selector, calendario, seleccionar","dateRangeCompName":"Rango de fechas","dateRangeCompDesc":"Un componente para seleccionar un intervalo de fechas, útil para sistemas de reservas o filtros.","dateRangeCompKeywords":"daterange, seleccionar, reservar, filtrar","timeCompName":"Tiempo","timeCompDesc":"Un componente de selección de hora para elegir horas concretas del día.","timeCompKeywords":"hora, selector, seleccionar, reloj","timeRangeCompName":"Rango temporal","timeRangeCompDesc":"Componente para seleccionar un intervalo de tiempo, utilizado a menudo en aplicaciones de programación.","timeRangeCompKeywords":"timerange, seleccionar, programación, duración","buttonCompName":"Botón Formulario","buttonCompDesc":"Un componente de botón versátil para enviar formularios, desencadenar acciones o navegar.","buttonCompKeywords":"botón, enviar, acción, navegar","linkCompName":"Enlace","linkCompDesc":"Un componente de visualización de hipervínculos para navegar o enlazar con recursos externos.","linkCompKeywords":"enlace, hipervínculo, navegación, externo","scannerCompName":"Escáner","scannerCompDesc":"Un componente para escanear códigos de barras, códigos QR y otros datos similares.","scannerCompKeywords":"escáner, código de barras, código QR, escanear","dropdownCompName":"Desplegable","dropdownCompDesc":"Un menú desplegable para mostrar de forma compacta una lista de opciones.","dropdownCompKeywords":"desplegable, menú, opciones, seleccionar","toggleButtonCompName":"Botón Alternar","toggleButtonCompDesc":"Un botón que puede alternar entre dos estados u opciones.","toggleButtonCompKeywords":"conmutar, botón, interruptor, estado","textCompName":"Visualización de texto","textCompDesc":"Un componente sencillo para mostrar contenido de texto estático o dinámico con formato Markdown incluido.","textCompKeywords":"texto, visualización, estático, dinámico","tableCompName":"Tabla","tableCompDesc":"Un componente de tabla enriquecido para mostrar datos en formato de tabla estructurada, con opciones de ordenación y filtrado, visualización de Datos en árbol y Filas extensibles.","tableCompKeywords":"tabla, datos, ordenar, filtrar","imageCompName":"Imagen","imageCompDesc":"Un componente para mostrar imágenes, compatible con varios formatos basados en datos URI o Base64.","imageCompKeywords":"imagen, visualización, medios, Base64","progressCompName":"Progreso","progressCompDesc":"Un indicador visual de progreso, utilizado normalmente para mostrar el estado de finalización de una tarea.","progressCompKeywords":"progreso, indicador, estado, tarea","progressCircleCompName":"Círculo de Progreso","progressCircleCompDesc":"Un indicador de progreso circular, utilizado a menudo para estados de carga o tareas con límite de tiempo.","progressCircleCompKeywords":"círculo, progreso, indicador, carga","fileViewerCompName":"Visor de archivos","fileViewerCompDesc":"Un componente para visualizar varios tipos de archivos, incluidos documentos e imágenes.","fileViewerCompKeywords":"archivo, visor, documento, imagen","dividerCompName":"Divisor","dividerCompDesc":"Componente divisor visual, utilizado para separar contenidos o secciones en un diseño.","dividerCompKeywords":"divisor, separador, disposición, diseño","qrCodeCompName":"Código QR","qrCodeCompDesc":"Un componente para mostrar códigos QR, útil para escanearlos rápidamente y transferir información.","qrCodeCompKeywords":"Código QR, escaneo, código de barras, información","formCompName":"Formulario","formCompDesc":"Un componente contenedor para construir formularios estructurados con varios tipos de entrada.","formCompKeywords":"formulario, entrada, contenedor, estructura","jsonSchemaFormCompName":"Formulario de esquema JSON","jsonSchemaFormCompDesc":"Un componente de formulario dinámico generado a partir de un esquema JSON.","jsonSchemaFormCompKeywords":"JSON, esquema, formulario, dinámico","containerCompName":"Contenedor","containerCompDesc":"Un contenedor de uso general para el diseño y la organización de los elementos de la interfaz de usuario.","containerCompKeywords":"contenedor, diseño, organización, IU","collapsibleContainerCompName":"Contenedor plegable","collapsibleContainerCompDesc":"Un contenedor que puede expandirse o colapsarse, ideal para gestionar la visibilidad del contenido.","collapsibleContainerCompKeywords":"plegable, contenedor, expandir, colapsar","tabbedContainerCompName":"Contenedor con pestañas","tabbedContainerCompDesc":"Un contenedor con navegación por pestañas para organizar el contenido en paneles separados.","tabbedContainerCompKeywords":"pestañas, contenedor, navegación, paneles","modalCompName":"Modal","modalCompDesc":"Un componente modal emergente para mostrar contenido, alertas o formularios en foco.","modalCompKeywords":"modal, popup, alerta, formulario","listViewCompName":"Ver lista","listViewCompDesc":"Un componente para mostrar una lista de elementos o datos, en cuyo interior puedes colocar otros componentes. Como un repetidor.","listViewCompKeywords":"lista, vista, visualización, repetidor","gridCompName":"Rejilla","gridCompDesc":"Un componente de cuadrícula flexible para crear diseños estructurados con filas y columnas como extensión del componente Vista Lista.","gridCompKeywords":"cuadrícula, diseño, filas, columnas","navigationCompName":"Navegación","navigationCompDesc":"Un componente de navegación para crear menús, migas de pan o pestañas para la navegación por el sitio.","navigationCompKeywords":"navegación, menú, migas de pan, pestañas","iframeCompName":"IFrame","iframeCompDesc":"Un componente de marco en línea para incrustar páginas web externas y aplicaciones o contenidos dentro de la aplicación.","iframeCompKeywords":"iframe, incrustar, página web, contenido","customCompName":"Componente personalizado","customCompDesc":"Un componente flexible y programable para crear elementos de interfaz de usuario únicos, definidos por el usuario y adaptados a tus necesidades específicas.","customCompKeywords":"personalizado, definido por el usuario, flexible, programable","moduleCompName":"Módulo","moduleCompDesc":"Utiliza Módulos para crear Micro-Apps diseñadas para encapsular funcionalidades o características específicas. Los módulos pueden incrustarse y reutilizarse en todas las aplicaciones.","moduleCompKeywords":"módulo, micro-app, funcionalidad, reutilizable","jsonExplorerCompName":"Explorador JSON","jsonExplorerCompDesc":"Un componente para explorar visualmente e interactuar con estructuras de datos JSON.","jsonExplorerCompKeywords":"JSON, explorador, datos, estructura","jsonEditorCompName":"Editor JSON","jsonEditorCompDesc":"Un componente editor para crear y modificar datos JSON con validación y resaltado de sintaxis.","jsonEditorCompKeywords":"JSON, editor, modificar, validar","treeCompName":"Árbol","treeCompDesc":"Un componente de estructura de árbol para mostrar datos jerárquicos, como sistemas de archivos u organigramas.","treeCompKeywords":"árbol, jerárquico, datos, estructura","treeSelectCompName":"Seleccionar árbol","treeSelectCompDesc":"Un componente de selección que presenta las opciones en formato de árbol jerárquico, permitiendo selecciones organizadas y anidadas.","treeSelectCompKeywords":"árbol, seleccionar, jerárquico, anidado","audioCompName":"Audio","audioCompDesc":"Un componente para incrustar contenido de audio, con controles para la reproducción y el ajuste del volumen.","audioCompKeywords":"audio, reproducción, sonido, música","videoCompName":"Vídeo","videoCompDesc":"Un componente multimedia para incrustar y reproducir contenidos de vídeo, compatible con varios formatos.","videoCompKeywords":"vídeo, multimedia, reproducción, incrustar","drawerCompName":"Cajón","drawerCompDesc":"Componente de un panel deslizante que puede utilizarse para navegación adicional o visualización de contenidos, y que suele emerger del borde de la pantalla.","drawerCompKeywords":"cajón, corredera, panel, navegación","chartCompName":"Gráfico","chartCompDesc":"Un componente versátil para visualizar datos mediante diversos tipos de tablas y gráficos.","chartCompKeywords":"tabla, gráfico, datos, visualización","carouselCompName":"Carrusel de imágenes","carouselCompDesc":"Un componente de carrusel giratorio para mostrar imágenes, banners o diapositivas de contenido.","carouselCompKeywords":"carrusel, imágenes, rotación, escaparate","imageEditorCompName":"Editor de imágenes","imageEditorCompDesc":"Un componente interactivo para editar y manipular imágenes, que ofrece diversas herramientas y filtros.","imageEditorCompKeywords":"imagen, editor, manipular, herramientas","mermaidCompName":"Cartas de sirenas","mermaidCompDesc":"Un componente para representar diagramas y organigramas complejos basados en la sintaxis Mermaid.","mermaidCompKeywords":"sirena, gráficos, diagramas, organigramas","calendarCompName":"Calendario","calendarCompDesc":"Un componente de calendario para mostrar fechas y eventos, con opciones de vistas de mes, semana o día.","calendarCompKeywords":"calendario, fechas, eventos, programación","signatureCompName":"Firma","signatureCompDesc":"Un componente para capturar firmas digitales, útil para procesos de aprobación y verificación.","signatureCompKeywords":"firma, digital, aprobación, verificación","jsonLottieCompName":"Animación Lottie","jsonLottieCompDesc":"Un componente para mostrar animaciones Lottie, que proporciona animaciones ligeras y escalables basadas en datos JSON.","jsonLottieCompKeywords":"lottie, animación, JSON, escalable","timelineCompName":"Cronología","timelineCompDesc":"Componente para mostrar acontecimientos o acciones en orden cronológico, representados visualmente a lo largo de una línea de tiempo lineal.","timelineCompKeywords":"cronología, acontecimientos, cronológico, historia","commentCompName":"Comentario","commentCompDesc":"Un componente para añadir y mostrar comentarios de los usuarios, que admite respuestas en hilos y la interacción de los usuarios.","commentCompKeywords":"comentario, debate, interacción con el usuario, respuesta","mentionCompName":"Menciona","mentionCompDesc":"Componente que permite mencionar usuarios o etiquetas dentro de un contenido de texto, utilizado normalmente en redes sociales o plataformas colaborativas.","mentionCompKeywords":"mencionar, etiquetar, usuario, redes sociales","responsiveLayoutCompName":"Diseño adaptable","responsiveLayoutCompDesc":"Un componente de diseño diseñado para adaptarse y responder a diferentes tamaños de pantalla y dispositivos, garantizando una experiencia de usuario coherente.","responsiveLayoutCompKeywords":"responsive, diseño, adaptar, tamaño pantalla"},"comp":{"menuViewDocs":"Ver documentación","menuViewPlayground":"Ver zona de juegos interactiva","menuUpgradeToLatest":"Actualizar a la última versión","nameNotEmpty":"No puede estar vacío","nameRegex":"Debe empezar por una letra y contener sólo letras, cifras y guiones bajos (_)","nameJSKeyword":"No puede ser una palabra clave JavaScript","nameGlobalVariable":"No puede ser un nombre de variable global","nameExists":"El nombre {nombre} ya existe","getLatestVersionMetaError":"No se ha podido obtener la última versión, inténtalo más tarde.","needNotUpgrade":"La versión actual ya es la última.","compNotFoundInLatestVersion":"Componente actual no encontrado en la última versión.","upgradeSuccess":"Actualizado correctamente a la última versión.","searchProp":"Busca en"},"jsonSchemaForm":{"retry":"Reintentar","resetAfterSubmit":"Restablecer después de enviar correctamente el formulario","jsonSchema":"Esquema JSON","uiSchema":"Esquema de IU","schemaTooltip":"Consulta","defaultData":"Datos de formulario precargados","dataDesc":"Datos del formulario actual","required":"Necesario","maximum":"El valor máximo es {valor}","minimum":"El valor mínimo es {valor}","exclusiveMaximum":"Debe ser inferior a {valor}","exclusiveMinimum":"Debe ser mayor que {valor}","multipleOf":"Debe ser múltiplo de {valor}","minLength":"Al menos {valor} Caracteres","maxLength":"Como máximo {valor} Caracteres","pattern":"Debe coincidir con el patrón {valor}","format":"Debe coincidir con el formato {valor}"},"select":{"inputValueDesc":"Valor de búsqueda de entrada"},"customComp":{"text":"Es un buen día.","triggerQuery":"Consulta desencadenante","updateData":"Actualizar datos","updateText":"¡También estoy de buen humor para desarrollar ahora mi propio componente personalizado con Lowcoder!","sdkGlobalVarName":"Lowcoder","data":"Datos que quieres pasar al Componente personalizado","code":"Código de tu componente personalizado"},"tree":{"selectType":"Selecciona el tipo","noSelect":"No Seleccionar","singleSelect":"Selección única","multiSelect":"Selección múltiple","checkbox":"Casilla de verificación","checkedStrategy":"Estrategia comprobada","showAll":"Todos los nodos","showParent":"Sólo nodos padre","showChild":"Nodos hijos únicos","autoExpandParent":"Auto Expandir Padre","checkStrictly":"Comprobar estrictamente","checkStrictlyTooltip":"Comprueba con precisión el Nodo del Árbol; el Nodo del Árbol padre y los Nodos del Árbol hijos no están asociados","treeData":"Datos del árbol","treeDataDesc":"Datos actuales del árbol","value":"Valores por defecto","valueDesc":"Valores actuales","expanded":"Valores ampliados","expandedDesc":"Valores ampliados actuales","defaultExpandAll":"Por defecto Expandir todos los nodos","showLine":"Mostrar línea","showLeafIcon":"Mostrar icono de hoja","treeDataAsia":"Asia","treeDataChina":"China","treeDataBeijing":"Pekín","treeDataShanghai":"Shanghai","treeDataJapan":"Japón","treeDataEurope":"Europa","treeDataEngland":"Inglaterra","treeDataFrance":"Francia","treeDataGermany":"Alemania","treeDataNorthAmerica":"América del Norte","helpLabel":"Etiqueta de nodo","helpValue":"Valor único del nodo en el árbol","helpChildren":"Niños Nodos","helpDisabled":"Desactiva el Nodo","helpSelectable":"Si el Nodo es Seleccionable (Tipo de Selección Simple/Múltiple)","helpCheckable":"Si mostrar casilla de verificación (Tipo de casilla de verificación)","helpDisableCheckbox":"Desactiva la casilla de verificación (Tipo de casilla de verificación)"},"moduleContainer":{"eventTest":"Prueba de Evento","methodTest":"Método de ensayo","inputTest":"Prueba de entrada"},"password":{"label":"Contraseña","visibilityToggle":"Conmutar visibilidad"},"richTextEditor":{"toolbar":"Personalizar la barra de herramientas","toolbarDescription":"Puedes personalizar la barra de herramientas. Consulta: https://quilljs.com/docs/modules/toolbar/ para más detalles.","placeholder":"Por favor, introduce...","hideToolbar":"Ocultar barra de herramientas","content":"Contenido","title":"Título","save":"Guarda","link":"Enlace: ","edit":"Edita","remove":"Elimina","defaultValue":"Contenido básico"},"numberInput":{"formatter":"Formato","precision":"Precisión","allowNull":"Permitir valor nulo","thousandsSeparator":"Mostrar separador de miles","controls":"Mostrar botones de aumento/disminución","step":"Paso","standard":"Estándar","percent":"Porcentaje"},"slider":{"step":"Paso","stepTooltip":"El valor debe ser mayor que 0 y divisible por (Máx-Mín)"},"rating":{"max":"Clasificación máxima","allowHalf":"Permitir la mitad de puntos de valoración"},"optionsControl":{"optionList":"Opciones","option":"Opción","optionI":"Opción {i}","viewDocs":"Ver documentos","tip":"Las variables \\'item\\' y \\'i\\' representan el valor y el índice de cada elemento de la matriz de datos"},"radio":{"options":"Opciones","horizontal":"Horizontal","horizontalTooltip":"La Disposición Horizontal Se Enrolla Cuando Se Queda Sin Espacio","vertical":"Vertical","verticalTooltip":"El diseño vertical siempre se mostrará en una sola columna","autoColumns":"Columna Auto","autoColumnsTooltip":"La disposición en autocolumnas reordena automáticamente el orden según lo permita el espacio y se muestra como varias columnas"},"cascader":{"options":"Datos JSON para mostrar selecciones en cascada"},"selectInput":{"valueDesc":"Valor seleccionado actualmente","selectedIndexDesc":"El índice del valor seleccionado actualmente, o -1 si no hay ningún valor seleccionado","selectedLabelDesc":"La etiqueta del valor seleccionado actualmente"},"file":{"typeErrorMsg":"Debe ser un número con una unidad de tamaño de archivo válida, o un número de bytes sin unidad.","fileEmptyErrorMsg":"Carga fallida. El tamaño del archivo está vacío.","fileSizeExceedErrorMsg":"Carga fallida. El tamaño del archivo supera el límite.","minSize":"Tamaño mínimo","minSizeTooltip":"El Tamaño Mínimo de los Archivos Subidos con Unidades de Tamaño de Archivo Opcionales (por ejemplo, \\'5kb\\', \\'10 MB\\'). Si no se proporciona ninguna unidad, el valor se considerará un número de bytes.","maxSize":"Tamaño máximo","maxSizeTooltip":"El tamaño máximo de los archivos subidos con unidades opcionales de tamaño de archivo (por ejemplo, \\'5kb\\', \\'10 MB\\'). Si no se proporciona ninguna unidad, el valor se considerará un número de bytes.","single":"Individual","multiple":"Múltiple","directory":"Directorio","upload":"Navega por","fileType":"Tipos de archivos","reference":"Consulta","fileTypeTooltipUrl":"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers","fileTypeTooltip":"Especificadores únicos de tipo de archivo","uploadType":"Tipo de carga","showUploadList":"Mostrar lista de cargas","maxFiles":"Archivos Max","filesValueDesc":"El contenido del archivo cargado actualmente está codificado en Base64","filesDesc":"Lista de los archivos cargados actualmente. Para más detalles, consulta","clearValueDesc":"Borrar todos los archivos","parseFiles":"Analizar archivos","parsedValueTooltip1":"Si parseFiles es True, los archivos cargados se convertirán en objetos, matrices o cadenas. Se puede acceder a los datos analizados a través de la matriz parsedValue.","parsedValueTooltip2":"Admite archivos Excel, JSON, CSV y de texto. Otros formatos devolverán un valor nulo."},"date":{"format":"Formato","formatTip":"Admite: \\AAAA-MM-DD HH:mm:ss\", \"AAAA-MM-DD\", \"Timestamp\".","reference":"Consulta","showTime":"Hora del espectáculo","start":"Fecha de inicio","end":"Fecha final","year":"Año","quarter":"Cuarto","month":"Mes","week":"Semana","date":"Fecha","clearAllDesc":"Borrar todo","resetAllDesc":"Restablecer todo","placeholder":"Seleccionar fecha","placeholderText":"Marcador de posición","startDate":"Fecha de inicio","endDate":"Fecha final"},"time":{"start":"Hora de inicio","end":"Fin de los tiempos","formatTip":"Soporte: \\'HH:mm:ss\\', \\'Timestamp\\'","format":"Formato","placeholder":"Selecciona Hora","placeholderText":"Marcador de posición","startTime":"Hora de inicio","endTime":"Fin de los tiempos"},"button":{"prefixIcon":"Icono Prefijo","suffixIcon":"Icono Sufijo","icon":"Icono","iconSize":"Tamaño del icono","button":"Botón Formulario","formToSubmit":"Formulario para enviar","default":"Por defecto","submit":"Envía","textDesc":"Texto mostrado actualmente en el botón","loadingDesc":"¿Está el Botón en Estado de Carga? Si es cierto, el botón actual está cargando","formButtonEvent":"Evento"},"link":{"link":"Enlace","textDesc":"Texto mostrado actualmente en el enlace","loadingDesc":"¿Está el Enlace en Estado de Carga? Si es Verdadero, el Enlace Actual Está Cargando"},"scanner":{"text":"Haz clic en Escanear","camera":"Cámara {índice}","changeCamera":"Cambiar cámara","continuous":"Escaneado continuo","uniqueData":"Ignorar datos duplicados","maskClosable":"Pulsa la Máscara para Cerrar","errTip":"Utiliza este componente bajo HTTPS o Localhost"},"dropdown":{"onlyMenu":"Pantalla sólo con etiqueta","textDesc":"Texto mostrado actualmente en el botón"},"textShow":{"text":"### 👋 Hola, {nombre}","valueTooltip":"Markdown admite la mayoría de etiquetas y atributos HTML. iframe, Script y otras etiquetas están desactivadas por motivos de seguridad.","verticalAlignment":"Alineación vertical","horizontalAlignment":"Alineación horizontal","textDesc":"Texto mostrado en el cuadro de texto actual"},"table":{"editable":"Editable","columnNum":"Columnas","viewModeResizable":"Ancho de columna ajustado por el usuario","viewModeResizableTooltip":"Si los usuarios pueden ajustar el ancho de columna.","showFilter":"Botón Mostrar filtro","showRefresh":"Mostrar botón Actualizar","showDownload":"Mostrar botón de descarga","columnSetting":"Botón Mostrar ajuste de columna","searchText":"Buscar texto","searchTextTooltip":"Buscar y Filtrar los Datos Presentados en la Tabla","showQuickJumper":"Mostrar saltador rápido","hideOnSinglePage":"Ocultar en una sola página","showSizeChanger":"Mostrar botón de cambio de tamaño","pageSizeOptions":"Opciones de tamaño de página","pageSize":"Tamaño de página","total":"Recuento total de filas","totalTooltip":"El valor por defecto es el número de elementos de datos actuales, que se pueden obtener de la consulta, por ejemplo: \\'{{query1.data[0].count}}\\'","filter":"Filtrar","filterRule":"Regla de filtrado","chooseColumnName":"Elegir columna","chooseCondition":"Elegir condición","clear":"Claro","columnShows":"Columnas","selectAll":"Seleccionar todo","and":"Y","or":"O","contains":"Contiene","notContain":"No contiene","equals":"Es igual a","isNotEqual":"No es igual","isEmpty":"Está vacío","isNotEmpty":"No está vacío","greater":"Mayor que","greaterThanOrEquals":"Mayor o igual que","lessThan":"Menos de","lessThanOrEquals":"Menor o igual que","action":"Acción","columnValue":"Valor de columna","columnValueTooltip":"\\'{{currentCell}}\\': Datos celulares actuales{{currentRow}}\\': Datos de la fila actual{{currentIndex}}\\': Índice de Datos Actuales (Empezando por 0)\\n Ejemplo: \\'{{CeldaActual * 5}}' Mostrar 5 Veces el Valor Original de los Datos.","imageSrc":"Fuente de la imagen","imageSize":"Tamaño de la imagen","columnTitle":"Título","sortable":"Clasificable","align":"Alineación","fixedColumn":"Columna fija","autoWidth":"Ancho automático","customColumn":"Columna personalizada","auto":"Auto","fixed":"Fijo","columnType":"Tipo de columna","float":"Flotador","prefix":"Prefijo","suffix":"Sufijo","text":"Texto","number":"Número","link":"Enlace","links":"Enlaces","tag":"Etiqueta","date":"Fecha","dateTime":"Fecha Hora","badgeStatus":"Estado","button":"Botón","image":"Imagen","boolean":"Booleano","rating":"Clasificación","progress":"Progreso","option":"Operación","optionList":"Lista de operaciones","option1":"Operación 1","status":"Estado","statusTooltip":"Valores opcionales: Correcto, Error, Predeterminado, Advertencia, Procesando","primaryButton":"Primaria","defaultButton":"Por defecto","type":"Tipo","tableSize":"Tamaño de la tabla","hideHeader":"Ocultar cabecera de tabla","fixedHeader":"Cabecera de tabla fija","fixedHeaderTooltip":"La cabecera será fija para la tabla desplazable verticalmente","fixedToolbar":"Barra de herramientas fija","fixedToolbarTooltip":"La barra de herramientas se fijará para la tabla desplazable verticalmente en función de la posición","hideBordered":"Ocultar borde de columna","deleteColumn":"Borrar columna","confirmDeleteColumn":"Confirmar Borrar columna: ","small":"S","middle":"M","large":"L","refreshButtonTooltip":"Los Datos Actuales Cambian, Pulsa para Regenerar la Columna.","changeSetDesc":"Un Objeto que Representa Cambios en una Tabla Editable, Sólo Contiene la Celda Cambiada. Las filas van primero y las columnas después.","selectedRowDesc":"Proporciona Datos de la Fila Seleccionada Actualmente, Indicando la Fila que Desencadena un Suceso de Clic Si el Usuario Pulsa un Botón/Link en la Fila","selectedRowsDesc":"Útil en el modo de selección múltiple, igual que SelectedRow","pageNoDesc":"Página de visualización actual, empezando por 1","pageSizeDesc":"Cuántas filas por página","sortColumnDesc":"El nombre de la columna ordenada actualmente seleccionada","sortDesc":"Si la fila actual está en orden descendente","pageOffsetDesc":"El inicio actual de la paginación, utilizado para paginar para obtener datos. Ejemplo: Select * from Usuarios Limit \\'{{table1.pageSize}}\\Desplazamiento{{table1.pageOffset}}\\'","displayDataDesc":"Datos mostrados en la tabla actual","selectedIndexDesc":"Índice seleccionado en Mostrar datos","filterDesc":"Parámetros de filtrado de tablas","dataDesc":"Los datos JSON de la tabla","saveChanges":"Guardar cambios","cancelChanges":"Cancelar cambios","rowSelectChange":"Cambio de selección de fila","rowClick":"Fila Clic","rowExpand":"Fila Ampliar","filterChange":"Cambio de filtro","sortChange":"Ordenar Cambio","pageChange":"Cambio de página","refresh":"Actualiza","rowColor":"Color de fila condicional","rowColorDesc":"Establece Condicionalmente el Color de la Fila en Función de las Variables Opcionales: FilaActual, ÍndiceOriginalActual, ÍndiceActual, TítuloDeColumna. Por ejemplo \\'{{ filaactual.id > 3 ? \"verde%r@\\\" : \"rojo%r@\\\" }}\\'","cellColor":"Color de celda condicional","cellColorDesc":"Establece condicionalmente el color de la celda en función del valor de la celda utilizando CeldaActual. Por ejemplo \\'{{ celdaactual == 3 ? \"verde%r@\\\" : \"rojo%r@\\\" }}\\'","saveChangesNotBind":"No se ha configurado ningún controlador de eventos para guardar los cambios. Por favor, vincula al menos un controlador de eventos antes de hacer clic.","dynamicColumn":"Utilizar la configuración dinámica de columnas","dynamicColumnConfig":"Ajuste de columna","dynamicColumnConfigDesc":"Configuración Dinámica de Columnas. Acepta una Matriz de Nombres de Columna. Todas las Columnas son Visibles por Defecto. Ejemplo: [%r@\\\"id%r@\\\", %r@\\\"name%r@\\\"]","position":"Posición","showDataLoadSpinner":"Mostrar la rueda giratoria durante la carga de datos","showValue":"Mostrar valor","expandable":"Ampliable","configExpandedView":"Configurar vista ampliada","toUpdateRowsDesc":"Una matriz de objetos para filas a actualizar en tablas editables.","empty":"Vacío","falseValues":"Texto cuando es falso","allColumn":"Todos","visibleColumn":"Visible","emptyColumns":"Actualmente no hay columnas visibles"},"image":{"src":"Fuente de la imagen","srcDesc":"La fuente de la imagen. Puede ser una URL, una ruta o una cadena Base64. Por ejemplo: data:image/png;base64, AAA... CCC","supportPreview":"Soporte Haz clic en Vista previa (zoom)","supportPreviewTip":"Efectivo cuando la fuente de la imagen es válida"},"progress":{"value":"Valor","valueTooltip":"El Porcentaje Completo como valor entre 0 y 100","showInfo":"Mostrar valor","valueDesc":"Valor de progreso actual, de 0 a 100","showInfoDesc":"Si mostrar el valor de progreso actual"},"fileViewer":{"invalidURL":"Introduce una URL válida o una cadena Base64","src":"URI del archivo","srcTooltip":"Previsualiza el contenido del enlace proporcionado incrustando HTML, también se pueden admitir datos codificados en base64, por ejemplo: data:application/pdf; base64,AAA... CCC","srcDesc":"La URI del archivo"},"divider":{"title":"Título","align":"Alineación","dashed":"Guiones","dashedDesc":"Si utilizar línea discontinua","titleDesc":"Título del divisor","alignDesc":"Alineación del título del divisor"},"QRCode":{"value":"Valor del contenido del código QR","valueTooltip":"El valor contiene un máximo de 2953 caracteres. El valor del código QR puede codificar varios tipos de datos, como mensajes de texto, URL, datos de contacto (VCard/meCard), credenciales de inicio de sesión Wi-Fi, direcciones de correo electrónico, números de teléfono, mensajes SMS, coordenadas de geolocalización, detalles de eventos del calendario, información de pago, direcciones de criptomonedas y enlaces de descarga de aplicaciones.","valueDesc":"El valor del contenido del código QR","level":"Nivel de tolerancia a fallos","levelTooltip":"Se refiere a la capacidad del código QR para ser escaneado aunque parte de él esté bloqueada. Cuanto más alto es el nivel, más complejo es el código.","includeMargin":"Mostrar margen","image":"Mostrar imagen en el centro","L":"L (Bajo)","M":"M (Medio)","Q":"Q (Cuartil)","H":"H (Alto)","maxLength":"El contenido es demasiado largo. Ajusta la longitud a menos de 2953 caracteres"},"jsonExplorer":{"indent":"Sangría de cada nivel","expandToggle":"Expandir árbol JSON","theme":"Tema del color","valueDesc":"Datos JSON actuales","default":"Por defecto","defaultDark":"Por defecto Oscuro","neutralLight":"Luz neutra","neutralDark":"Neutro Oscuro","azure":"Azure","darkBlue":"Azul oscuro"},"audio":{"src":"URI de la fuente de audio o cadena Base64","defaultSrcUrl":"https://cdn.pixabay.com/audio/2023/07/06/audio_e12e5bea9d.mp3","autoPlay":"Reproducción automática","loop":"Bucle","srcDesc":"URI de audio actual o cadena Base64 como data:audio/mpeg;base64,AAA... CCC","play":"Juega a","playDesc":"Se activa cuando se reproduce audio","pause":"Pausa","pauseDesc":"Se activa cuando se pausa el audio","ended":"Finalizado","endedDesc":"Se activa cuando el audio termina de reproducirse"},"video":{"src":"URI de la fuente de vídeo o cadena Base64","defaultSrcUrl":"https://www.youtube.com/watch?v=pRpeEdMmmQ0","poster":"URL del póster","defaultPosterUrl":"","autoPlay":"Reproducción automática","loop":"Bucle","controls":"Ocultar controles","volume":"Volumen","playbackRate":"Velocidad de reproducción","posterTooltip":"El valor por defecto es el primer fotograma del vídeo","autoPlayTooltip":"Después de cargar el vídeo, se reproducirá automáticamente. Si cambias este valor de Verdadero a Falso, el vídeo se pausará. (Si se establece un Póster, se reproducirá mediante el Botón de Póster)","controlsTooltip":"Ocultar controles de reproducción de vídeo. Puede no ser totalmente compatible con todas las fuentes de vídeo.","volumeTooltip":"Ajustar el Volumen del Reproductor, Entre 0 y 1","playbackRateTooltip":"Ajustar la velocidad del reproductor, entre 1 y 2","srcDesc":"URI de audio actual o cadena Base64 como data:video/mp4;base64, AAA... CCC","play":"Juega a","playDesc":"Se activa cuando se reproduce el vídeo","pause":"Pausa","pauseDesc":"Se activa al pausar el vídeo","load":"Carga","loadDesc":"Se activa cuando el recurso de vídeo ha terminado de cargarse","ended":"Finalizado","endedDesc":"Se activa cuando el vídeo termina de reproducirse","currentTimeStamp":"La posición actual de reproducción del vídeo en segundos","duration":"La duración total del vídeo en segundos"},"media":{"playDesc":"Inicia la reproducción del medio.","pauseDesc":"Pausa la reproducción multimedia.","loadDesc":"Restablece el Medio al Principio y Reinicia Seleccionando el Recurso Multimedia.","seekTo":"Buscar hasta el número de segundos dado, o fracción si la cantidad está entre 0 y 1","seekToAmount":"Número de segundos, o fracción si está entre 0 y 1","showPreview":"Avance del espectáculo"},"rangeSlider":{"start":"Valor inicial","end":"Valor final","step":"Tamaño del paso","stepTooltip":"Granularidad del deslizador, el valor debe ser mayor que 0 y divisible por (Max-Min)"},"iconControl":{"selectIcon":"Selecciona un icono","insertIcon":"Insertar un icono","insertImage":"Insertar una imagen o "},"millisecondsControl":{"timeoutTypeError":"Por favor, introduce el periodo de tiempo de espera correcto en ms, la entrada actual es: {valor}","timeoutLessThanMinError":"La entrada debe ser mayor que {izquierda}, la entrada actual es: {valor}"},"selectionControl":{"single":"Individual","multiple":"Múltiple","close":"Cerrar","mode":"Seleccionar modo"},"container":{"title":"Título del contenedor mostrado"},"drawer":{"closePosition": "Colocación de los cerca","placement":"Colocación de los cajones","size":"Talla","top":"Arriba","right":"A la derecha","bottom":"Fondo","left":"Izquierda","widthTooltip":"Píxel o Porcentaje, por ejemplo 520, 60%.","heightTooltip":"Píxel, por ejemplo 378","openDrawerDesc":"Cajón abierto","closeDrawerDesc":"Cerrar cajón","width":"Ancho del cajón","height":"Altura del cajón"},"meeting":{"logLevel":"Agora SDK Nivel de registro","placement":"Colocación del cajón de reunión","meeting":"Ajustes de la reunión","cameraView":"Vista de cámara","cameraViewDesc":"Vista de cámara del usuario local (anfitrión)","screenShared":"Pantalla compartida","screenSharedDesc":"Pantalla compartida por el usuario local (anfitrión)","audioUnmuted":"Audio sin silenciar","audioMuted":"Audio silenciado","videoClicked":"Vídeo pulsado","videoOff":"Vídeo apagado","videoOn":"Vídeo","size":"Talla","top":"Arriba","host":"Anfitrión de la Sala de Reuniones. Tendrías que gestionar el anfitrión como una Aplicación Lógica propia","participants":"Participantes de la Sala de Reuniones","shareScreen":"Pantalla compartida por el usuario local","appid":"ID de la aplicación Ágora","meetingName":"Nombre de la reunión","localUserID":"ID de usuario del host","userName":"Nombre de usuario del host","rtmToken":"Ficha Agora RTM","rtcToken":"Ficha Agora RTC","noVideo":"Sin vídeo","profileImageUrl":"URL de la imagen del perfil","right":"A la derecha","bottom":"Fondo","videoId":"ID del flujo de vídeo","audioStatus":"Estado del audio","left":"Izquierda","widthTooltip":"Píxel o Porcentaje, por ejemplo 520, 60%.","heightTooltip":"Píxel, por ejemplo 378","openDrawerDesc":"Cajón abierto","closeDrawerDesc":"Cerrar cajón","width":"Ancho del cajón","height":"Altura del cajón","actionBtnDesc":"Botón de acción","broadCast":"Transmitir mensajes","title":"Título de la reunión","meetingCompName":"Controlador de Reuniones Agora","sharingCompName":"Compartir pantalla Stream","videoCompName":"Flujo de cámara","videoSharingCompName":"Compartir pantalla Stream","meetingControlCompName":"Botón de control","meetingCompDesc":"Componente de la reunión","meetingCompControls":"Control de reuniones","meetingCompKeywords":"Reunión Ágora, Reunión Web, Colaboración","iconSize":"Tamaño del icono","userId":"ID de usuario del host","roomId":"Identificación de la habitación","meetingActive":"Reunión en curso","messages":"Mensajes emitidos"},"settings":{"title":"Ajustes","userGroups":"Grupos de usuarios","organization":"Espacios de trabajo","audit":"Registros de auditoría","theme":"Temas","plugin":"Plugins","advanced":"Avanzado","lab":"Laboratorio","branding":"Marca","oauthProviders":"Proveedores OAuth","appUsage":"Registros de uso de la aplicación","environments":"Entornos","premium":"Premium"},"memberSettings":{"admin":"Admin","adminGroupRoleInfo":"El administrador puede gestionar los miembros y recursos del grupo","adminOrgRoleInfo":"Los administradores son propietarios de todos los recursos y pueden gestionar grupos.","member":"Miembro","memberGroupRoleInfo":"Los miembros pueden ver a los miembros del grupo","memberOrgRoleInfo":"Los miembros sólo pueden utilizar o visitar los recursos a los que tienen acceso.","title":"Miembros","createGroup":"Crear grupo","newGroupPrefix":"Nuevo Grupo ","allMembers":"Todos los miembros","deleteModalTitle":"Eliminar este grupo","deleteModalContent":"No se puede restaurar el grupo eliminado. ¿Estás Seguro de Borrar el Grupo?","addMember":"Añadir miembros","nameColumn":"Nombre de usuario","joinTimeColumn":"Hora de incorporación","actionColumn":"Operación","roleColumn":"Papel","exitGroup":"Grupo de salida","moveOutGroup":"Eliminar del Grupo","inviteUser":"Invitar a miembros","exitOrg":"Deja","exitOrgDesc":"¿Estás seguro de que quieres dejar este espacio de trabajo?","moveOutOrg":"Elimina","moveOutOrgDescSaasMode":"¿Estás seguro de que quieres eliminar al usuario {nombre} de este espacio de trabajo?","moveOutOrgDesc":"¿Estás seguro de que quieres eliminar al usuario {nombre}? Esta acción no se puede recuperar.","devGroupTip":"Los miembros del Grupo de Desarrolladores tienen privilegios para crear aplicaciones y fuentes de datos.","lastAdminQuit":"El último administrador no puede salir.","organizationNotExist":"El espacio de trabajo actual no existe","inviteUserHelp":"Puedes copiar el enlace de invitación para enviarlo al usuario","inviteUserLabel":"Enlace de invitación:","inviteCopyLink":"Copiar enlace","inviteText":"{userName} Te invita a unirte al espacio de trabajo \"{organization}%r@\\\", Haz clic en el enlace para unirte: {inviteLink}","groupName":"Nombre del grupo","createTime":"Crear Tiempo","manageBtn":"Gestiona","userDetail":"Detalle","syncDeleteTip":"Este grupo ha sido eliminado de la Agenda Fuente","syncGroupTip":"Este grupo es un grupo de sincronización de la Agenda y no se puede editar"},"orgSettings":{"newOrg":"Nuevo espacio de trabajo (Organización)","title":"Espacio de trabajo","createOrg":"Crear espacio de trabajo (Organización)","deleteModalTitle":"¿Estás seguro de eliminar este espacio de trabajo?","deleteModalContent":"Estás a punto de Eliminar este Espacio de Trabajo {permanentementeEliminado}. Una vez Eliminado, el Espacio de Trabajo {noSeRestaura}.","permanentlyDelete":"Permanentemente","notRestored":"No se puede restaurar","deleteModalLabel":"Introduce el nombre del espacio de trabajo {nombre} para confirmar la operación:","deleteModalTip":"Introduce el nombre del espacio de trabajo","deleteModalErr":"El nombre del espacio de trabajo es incorrecto","deleteModalBtn":"Borra","editOrgTitle":"Editar información del espacio de trabajo","orgNameLabel":"Nombre del espacio de trabajo:","orgNameCheckMsg":"El nombre del espacio de trabajo no puede estar vacío","orgLogo":"Logotipo del Espacio de Trabajo:","logoModify":"Modificar imagen","inviteSuccessMessage":"Únete con éxito al espacio de trabajo","inviteFailMessage":"Error al unirse al espacio de trabajo","uploadErrorMessage":"Error de carga","orgName":"Nombre del espacio de trabajo"},"freeLimit":"Prueba gratuita","tabbedContainer":{"switchTab":"Pestaña Interruptor","switchTabDesc":"Se activa al cambiar de pestaña","tab":"Fichas","atLeastOneTabError":"El Contenedor de Pestañas Guarda al Menos Una Pestaña","selectedTabKeyDesc":"Pestaña \"Seleccionado actualmente","iconPosition":"Icono Posición"},"formComp":{"containerPlaceholder":"Arrastra componentes desde el panel derecho o","openDialogButton":"Generar un Formulario a partir de una de tus Fuentes de Datos","resetAfterSubmit":"Reiniciar después de enviar correctamente","initialData":"Datos iniciales","disableSubmit":"Desactivar Enviar","success":"Formulario generado correctamente","selectCompType":"Selecciona el tipo de componente","dataSource":"Fuente de datos: ","selectSource":"Seleccionar fuente","table":"Tabla: ","selectTable":"Seleccionar tabla","columnName":"Nombre de la columna","dataType":"Tipo de datos","compType":"Tipo de componente","required":"Necesario","generateForm":"Generar formulario","compSelectionError":"Tipo de columna no configurada","compTypeNameError":"No se ha podido obtener el nombre del tipo de componente","noDataSourceSelected":"No se ha seleccionado ninguna fuente de datos","noTableSelected":"No hay mesa seleccionada","noColumn":"Sin columna","noColumnSelected":"Sin columna seleccionada","noDataSourceFound":"No se ha encontrado ninguna fuente de datos compatible. Crear una nueva fuente de datos","noTableFound":"No se encontraron tablas en esta fuente de datos, por favor selecciona otra fuente de datos","noColumnFound":"No se ha encontrado ninguna columna compatible en esta tabla. Selecciona otra tabla","formTitle":"Título del formulario","name":"Nombre","nameTooltip":"El Nombre del Atributo en los Datos del Formulario, si se deja en blanco, es por defecto el Nombre del Componente","notSupportMethod":"Métodos no admitidos: ","notValidForm":"El formulario no es válido","resetDesc":"Restablecer los datos del formulario al valor por defecto","clearDesc":"Borrar datos del formulario","setDataDesc":"Establecer datos del formulario","valuesLengthError":"Número de parámetro Error","valueTypeError":"Tipo de parámetro Error","dataDesc":"Datos del formulario actual","loadingDesc":"¿Si el formulario está cargando?"},"modalComp":{"close":"Cerrar","closeDesc":"Se activa cuando se cierra el cuadro de diálogo modal","openModalDesc":"Abrir el Cuadro de Diálogo","closeModalDesc":"Cerrar el Cuadro de Diálogo","visibleDesc":"¿Es Visible? Si es Verdadero, Aparecerá el Cuadro de Diálogo Actual","modalHeight":"Altura modal","modalHeightTooltip":"Píxel, Ejemplo: 222","modalWidth":"Anchura modal","modalWidthTooltip":"Número o porcentaje, Ejemplo: 520, 60%"},"listView":{"noOfRows":"Recuento de filas","noOfRowsTooltip":"Número de filas de la lista - Suele establecerse en una variable (por ejemplo, \\'{{query1.data.length}}\\') para presentar los resultados de la consulta","noOfColumns":"Recuento de columnas","itemIndexName":"Elemento de datos Índice Nombre","itemIndexNameDesc":"El nombre de la variable que se refiere al índice del elemento, por defecto como {por defecto}.","itemDataName":"Elemento de datos Nombre del objeto","itemDataNameDesc":"El nombre de la variable que se refiere al objeto de datos del elemento, por defecto como {default}.","itemsDesc":"Exponer datos de componentes en lista","dataDesc":"Los datos JSON utilizados en la lista actual","dataTooltip":"Si sólo pones un Número, Este Campo Se Considerará Como Recuento De Filas, Y Los Datos Se Considerarán Vacíos."},"navigation":{"addText":"Añadir elemento de submenú","logoURL":"Navegación Logo URL","horizontalAlignment":"Alineación horizontal","logoURLDesc":"Puedes mostrar un Logotipo en el lado izquierdo introduciendo un Valor URI o una Cadena Base64 como data:image/png;base64,AAA... CCC","itemsDesc":"Elementos del menú de navegación jerárquica"},"droppadbleMenuItem":{"subMenu":"Submenú {número}"},"navItemComp":{"active":"Activo"},"iframe":{"URLDesc":"La URL de origen del contenido del IFrame. Asegúrate de que la URL es HTTPS o localhost. Asegúrate también de que la URL no está bloqueada por la Política de Seguridad de Contenidos (CSP) del navegador. La cabecera \\'X-Frame-Options\\' no debe tener el valor \\'DENY\\' o \\'SAMEORIGIN\\'.","allowDownload":"Permitir descargas","allowSubmitForm":"Permitir Enviar Formulario","allowMicrophone":"Permitir micrófono","allowCamera":"Permitir cámara","allowPopup":"Permitir ventanas emergentes"},"switchComp":{"defaultValue":"Valor booleano por defecto","open":"En","close":"Fuera de","openDesc":"Se activa al encender el interruptor","closeDesc":"Se activa cuando el interruptor está apagado","valueDesc":"Estado actual del interruptor"},"signature":{"tips":"Texto de sugerencia","signHere":"Firma aquí","showUndo":"Mostrar Deshacer","showClear":"Mostrar Borrar"},"localStorageComp":{"valueDesc":"Todos los datos almacenados actualmente","setItemDesc":"Añadir un elemento","removeItemDesc":"Eliminar un elemento","clearItemDesc":"Borrar todos los artículos"},"utilsComp":{"openUrl":"Abrir URL","openApp":"Abrir App","copyToClipboard":"Copiar al portapapeles","downloadFile":"Descargar archivo"},"messageComp":{"info":"Enviar una notificación","success":"Enviar una notificación de éxito","warn":"Enviar una notificación de advertencia","error":"Enviar una notificación de error"},"themeComp":{"switchTo":"Cambiar tema"},"transformer":{"preview":"Vista previa","docLink":"Leer más sobre Transformers...","previewSuccess":"Vista previa Éxito","previewFail":"Vista previa Fracaso","deleteMessage":"Eliminar Transformador con éxito. Puedes usar {undoKey} para Deshacer.","documentationText":"Los Transformadores están diseñados para transformar datos y reutilizar tu código JavaScript multilínea. Utiliza Transformadores para adaptar datos de consultas o componentes a las necesidades de tu App local. A diferencia de la consulta JavaScript, el transformador está diseñado para realizar operaciones de sólo lectura, lo que significa que no puedes lanzar una consulta o actualizar un estado temporal dentro de un transformador."},"temporaryState":{"value":"Valor inicial","valueTooltip":"El valor inicial almacenado en el estado temporal puede ser cualquier valor JSON válido.","docLink":"Leer más sobre los Estados Temporales...","pathTypeError":"La ruta debe ser una cadena o una matriz de valores","unStructuredError":"Los datos no estructurados {prev} no pueden ser actualizados por {path}.","valueDesc":"Valor Temporal del Estado","deleteMessage":"El Estado Temporal se Borra con Éxito. Puedes Usar {undoKey} para Deshacer.","documentationText":"Los estados temporales en Lowcoder son una potente función utilizada para gestionar variables complejas que actualizan dinámicamente el estado de los componentes de tu aplicación. Estos estados actúan como almacenamiento intermedio o transitorio de datos que pueden cambiar con el tiempo debido a interacciones del usuario u otros procesos."},"dataResponder":{"data":"Datos","dataDesc":"Datos del respondedor de datos actual","dataTooltip":"Cuando se modifiquen estos datos, se desencadenarán acciones posteriores.","docLink":"Más información sobre los respondedores de datos...","deleteMessage":"El Respondedor de datos se ha eliminado correctamente. Puedes utilizar {undoKey} para Deshacer.","documentationText":"Al desarrollar una aplicación, puedes asignar eventos a los componentes para controlar los cambios en datos concretos. Por ejemplo, un componente Tabla puede tener eventos como %r@\\\"Cambio de selección de fila%r@\\\", %r@\\\"Cambio de filtro%r@\\\", %r@\\\"Cambio de ordenación%r@\\\" y %r@\\\"Cambio de página%r@\\\" para controlar los cambios en la propiedad Fila seleccionada. Sin embargo, para los cambios en estados temporales, transformadores o resultados de consulta, en los que no se dispone de eventos estándar, se utilizan los Respondedores de datos. Te permiten detectar y reaccionar ante cualquier modificación de los datos."},"theme":{"title":"Temas","createTheme":"Crear tema","themeName":"Nombre del tema:","themeNamePlaceholder":"Introduce un nombre de tema","defaultThemeTip":"Tema por defecto:","createdThemeTip":"El tema que has creado:","option":"Opción{índice}","input":"Entrada","confirm":"Ok","emptyTheme":"No hay temas disponibles","click":"","toCreate":"","nameColumn":"Nombre","defaultTip":"Por defecto","updateTimeColumn":"Hora de actualización","edit":"Edita","cancelDefaultTheme":"Desactivar tema por defecto","setDefaultTheme":"Establecer como tema por defecto","copyTheme":"Tema duplicado","setSuccessMsg":"Ajuste superado","cancelSuccessMsg":"Desajuste conseguido","deleteSuccessMsg":"Supresión superada","checkDuplicateNames":"El nombre del tema ya existe, por favor, introdúcelo de nuevo","copySuffix":" Copia","saveSuccessMsg":"Guardado correctamente","leaveTipTitle":"Consejos","leaveTipContent":"¿Aún no te has salvado, confirma que te vas?","leaveTipOkText":"Deja","goList":"Volver a la lista","saveBtn":"Guarda","mainColor":"Colores principales","text":"Colores del texto","defaultTheme":"Por defecto","yellow":"Amarillo","green":"Verde","previewTitle":"Vista previa del tema\\nComponentes de ejemplo que utilizan los colores de tu tema","dateColumn":"Fecha","emailColumn":"Envía un correo electrónico a","phoneColumn":"Teléfono","subTitle":"Título","linkLabel":"Enlace","linkUrl":"app.lowcoder.nube","progressLabel":"Progreso","sliderLabel":"Deslizador","radioLabel":"Radio","checkboxLabel":"Casilla de verificación","buttonLabel":"Botón Formulario","switch":"Interruptor","previewDate":"16/10/2022","previewEmail1":"ted.com","previewEmail2":"skype.com","previewEmail3":"imgur.com","previewEmail4":"globo.com","previewPhone1":"+63-317-333-0093","previewPhone2":"+30-668-580-6521","previewPhone3":"+86-369-925-2071","previewPhone4":"+7-883-227-8093","chartPreviewTitle":"Vista previa del estilo de gráfico","chartSpending":"Gastar","chartBudget":"Presupuesto","chartAdmin":"Administración","chartFinance":"Finanzas","chartSales":"Ventas","chartFunnel":"Gráfico de embudo","chartShow":"Mostrar","chartClick":"Haz clic en","chartVisit":"Visita","chartQuery":"Consulta","chartBuy":"Comprar"},"pluginSetting":{"title":"Plugins","npmPluginTitle":"Plugins npm","npmPluginDesc":"Configurar plugins npm para todas las aplicaciones del espacio de trabajo actual.","npmPluginEmpty":"No se han añadido plugins npm.","npmPluginAddButton":"Añadir un plugin npm","saveSuccess":"Guardado correctamente"},"advanced":{"title":"Avanzado","defaultHomeTitle":"Página de inicio por defecto","defaultHomeHelp":"La página de inicio es la aplicación que todos los no desarrolladores verán por defecto cuando se conecten. Nota: Asegúrate de que la aplicación seleccionada es accesible para los no desarrolladores.","defaultHomePlaceholder":"Selecciona la página de inicio predeterminada","saveBtn":"Guarda","preloadJSTitle":"Precargar JavaScript","preloadJSHelp":"Configurar código JavaScript precargado para todas las aplicaciones del espacio de trabajo actual.","preloadCSSTitle":"Precargar CSS","preloadCSSHelp":"Configura el código CSS precargado para todas las aplicaciones del espacio de trabajo actual.","preloadCSSApply":"Aplicar a la página de inicio del espacio de trabajo","preloadLibsTitle":"Biblioteca JavaScript","preloadLibsHelp":"Configura Bibliotecas JavaScript Precargadas para Todas las Aplicaciones en el Espacio de Trabajo Actual, y el Sistema Tiene Incorporadas lodash, day.js, uuid, numbro para Uso Directo. Las Bibliotecas JavaScript Se Cargan Antes De Inicializar La Aplicación, Por Lo Que Hay Un Cierto Impacto En El Rendimiento De La Aplicación.","preloadLibsEmpty":"No se han añadido bibliotecas JavaScript","preloadLibsAddBtn":"Añadir una biblioteca","saveSuccess":"Guardado correctamente","AuthOrgTitle":"Pantalla de bienvenida al espacio de trabajo","AuthOrgDescrition":"La URL para que tus usuarios inicien sesión en el espacio de trabajo actual."},"branding":{"title":"Marca","logoTitle":"Logotipo","logoHelp":"Sólo .JPG, .SVG o .PNG","faviconTitle":"Favicon","faviconHelp":"Sólo .JPG, .SVG o .PNG","brandNameTitle":"Marca","headColorTitle":"Color de la cabeza","save":"Guarda","saveSuccessMsg":"Guardado correctamente","upload":"Haz clic para cargar"},"networkMessage":{"0":"No se ha podido conectar con el servidor, comprueba la red","401":"Autenticación fallida, por favor, inicia sesión de nuevo","403":"Sin permiso, ponte en contacto con el administrador para obtener autorización","500":"Servicio ocupado, inténtalo más tarde","timeout":"Tiempo de espera de la solicitud"},"share":{"title":"Comparte","viewer":"Visor","editor":"Editor","owner":"Propietario","datasourceViewer":"Se puede utilizar","datasourceOwner":"Puede gestionar"},"debug":{"title":"Título","switch":"Componente del interruptor: "},"module":{"emptyText":"Sin datos","circularReference":"Referencia circular, ¡no se puede utilizar el módulo/aplicación actual!","emptyTestInput":"El módulo actual no tiene entrada para comprobar","emptyTestMethod":"El módulo actual no tiene ningún método para probar","name":"Nombre","input":"Entrada","params":"Parámetros","emptyParams":"No se ha añadido ningún parámetro","emptyInput":"No se ha añadido ninguna entrada","emptyMethod":"No se ha añadido ningún método","emptyOutput":"No se ha añadido ninguna salida","data":"Datos","string":"Cadena","number":"Número","array":"Matriz","boolean":"Booleano","query":"Consulta","autoScaleCompHeight":"Básculas de altura de componentes con contenedor","excuteMethod":"Ejecutar método {nombre}","method":"Método","action":"Acción","output":"Salida","nameExists":"Nombre {name} Ya existe","eventTriggered":"Evento {nombre} activado","globalPromptWhenEventTriggered":"Muestra un aviso global cuando se activa un evento","emptyEventTest":"El módulo actual no tiene eventos que probar","emptyEvent":"No se ha añadido ningún evento","event":"Evento"},"resultPanel":{"returnFunction":"El valor de retorno es una función.","consume":"{tiempo}","JSON":"Mostrar JSON"},"createAppButton":{"creating":"Crear...","created":"Crear {nombre}"},"apiMessage":{"authenticationFail":"Ha fallado la autenticación de usuario, por favor, inicia sesión de nuevo","verifyAccount":"Necesidad de verificar la cuenta","functionNotSupported":"La versión actual no soporta esta función. Ponte en contacto con el equipo comercial de Lowcoder para actualizar tu cuenta."},"globalErrorMessage":{"createCompFail":"Crear componente {comp} Fallido","notHandledError":"{método} Método no ejecutado"},"aggregation":{"navLayout":"Barra de navegación","chooseApp":"Elegir aplicación","iconTooltip":"Admite enlace src de imagen o cadena base64 como data:image/png;base64,AAA... CCC","hideWhenNoPermission":"Oculto para usuarios no autorizados","queryParam":"Parámetros de consulta URL","hashParam":"Parámetros Hash de URL","tabBar":"Barra de pestañas","emptyTabTooltip":"Configurar esta página en el panel derecho"},"appSetting":{"450":"450px (Teléfono)","800":"800px (Tableta)","1440":"1440px (Portátil)","1920":"1920px (Pantalla ancha)","3200":"3200px (Pantalla supergrande)","title":"Configuración general de la aplicación","autofill":"Autorrelleno","userDefined":"Personalizado","default":"Por defecto","tooltip":"Cerrar la ventana emergente después de ajustar","canvasMaxWidth":"Ancho máximo del lienzo para esta aplicación","userDefinedMaxWidth":"Ancho máximo personalizado","inputUserDefinedPxValue":"Introduce un valor de píxel personalizado","maxWidthTip":"La anchura máxima debe ser mayor o igual que 350","themeSetting":"Tema Estilo Aplicado","themeSettingDefault":"Por defecto","themeCreate":"Crear tema"},"customShortcut":{"title":"Atajos personalizados","shortcut":"Atajo","action":"Acción","empty":"Sin atajos","placeholder":"Pulsa Atajo","otherPlatform":"Otros","space":"Espacio"},"profile":{"orgSettings":"Configuración del espacio de trabajo","switchOrg":"Cambiar de espacio de trabajo","joinedOrg":"Mis espacios de trabajo","createOrg":"Crear espacio de trabajo","logout":"Cerrar sesión","personalInfo":"Mi perfil","bindingSuccess":"Vinculación {nombreFuente} Éxito","uploadError":"Error de carga","editProfilePicture":"Modifica","nameCheck":"El nombre no puede estar vacío","name":"Nombre: ","namePlaceholder":"Introduce tu nombre","toBind":"Encuadernar","binding":"Es vinculante","bindError":"Error de parámetro, actualmente no admitido Vinculación.","bindName":"Enlazar {nombre}","loginAfterBind":"Después de vincular, puedes utilizar {nombre} para iniciar sesión","bindEmail":"Enlazar correo electrónico:","email":"Envía un correo electrónico a","emailCheck":"Introduce una dirección de correo electrónico válida","emailPlaceholder":"Introduce tu dirección de correo electrónico","submit":"Envía","bindEmailSuccess":"Éxito de la encuadernación por correo electrónico","passwordModifiedSuccess":"Contraseña cambiada correctamente","passwordSetSuccess":"Contraseña establecida correctamente","oldPassword":"Contraseña antigua:","inputCurrentPassword":"Introduce tu contraseña actual","newPassword":"Nueva contraseña:","inputNewPassword":"Introduce tu nueva contraseña","confirmNewPassword":"Confirma la nueva contraseña:","inputNewPasswordAgain":"Vuelve a introducir tu nueva contraseña","password":"Contraseña:","modifyPassword":"Modificar contraseña","setPassword":"Establecer contraseña","alreadySetPassword":"Conjunto de contraseñas","setPassPlaceholder":"Puedes iniciar sesión con tu contraseña","setPassAfterBind":"Puedes establecer la contraseña después de vincular la cuenta","socialConnections":"Conexiones sociales"},"shortcut":{"shortcutList":"Atajos de teclado","click":"Haz clic en","global":"Global","toggleShortcutList":"Alternar atajos de teclado","editor":"Editor","toggleLeftPanel":"Alternar panel izquierdo","toggleBottomPanel":"Alternar panel inferior","toggleRightPanel":"Alternar panel derecho","toggleAllPanels":"Conmutar todos los paneles","preview":"Vista previa","undo":"Deshacer","redo":"Rehaz","showGrid":"Mostrar cuadrícula","component":"Componente","multiSelect":"Seleccionar varios","selectAll":"Seleccionar todo","copy":"Copia","cut":"Corta","paste":"Pega","move":"Muévete","zoom":"Cambia el tamaño de","delete":"Borra","deSelect":"Deselecciona","queryEditor":"Editor de consultas","excuteQuery":"Ejecutar consulta actual","editBox":"Editor de texto","formatting":"Formato","openInLeftPanel":"Abrir en el panel izquierdo"},"help":{"videoText":"Visión general","onBtnText":"OK","permissionDenyTitle":"💡 ¿No se puede crear una nueva aplicación o fuente de datos?","permissionDenyContent":"No tienes permiso para crear la aplicación y la fuente de datos. Ponte en contacto con el Administrador para unirte al Grupo de Desarrolladores.","appName":"Aplicación Tutorial","chat":"Chatea con nosotros","docs":"Ver documentación","editorTutorial":"Tutorial del editor","update":"¿Qué hay de nuevo?","version":"Versión","versionWithColon":"Versión: ","submitIssue":"Enviar un asunto"},"header":{"nameCheckMessage":"El nombre no puede estar vacío","viewOnly":"Ver sólo","recoverAppSnapshotTitle":"¿Restaurar esta versión?","recoverAppSnapshotContent":"Restaurar la aplicación actual a la versión creada en {tiempo}.","recoverAppSnapshotMessage":"Restaurar esta versión","returnEdit":"Volver al editor","deploy":"Publica","export":"Exportar a JSON","editName":"Editar nombre","duplicate":"Duplicar {tipo}","snapshot":"Historia","scriptsAndStyles":"Guiones y estilo","appSettings":"Ajustes de la aplicación","preview":"Vista previa","editError":"Modo de Vista Previa de la Historia, no se admite ninguna operación.","clone":"Clon","editorMode_layout":"Disposición","editorMode_logic":"Lógica","editorMode_both":"Ambos"},"userAuth":{"registerByEmail":"Inscríbete","email":"Correo electrónico:","inputEmail":"Introduce tu dirección de correo electrónico","inputValidEmail":"Introduce una dirección de correo electrónico válida","register":"Inscríbete","userLogin":"Regístrate","login":"Regístrate","bind":"Encuaderna","passwordCheckLength":"Al menos {min} Personajes","passwordCheckContainsNumberAndLetter":"Debe contener letras y números","passwordCheckSpace":"No puede contener espacios en blanco","welcomeTitle":"Bienvenido a {productName}","inviteWelcomeTitle":"{username} Te invito a conectarte {productName}","terms":"Términos","privacy":"Política de privacidad","registerHint":"He leído y acepto el","chooseAccount":"Elige tu cuenta","signInLabel":"Iniciar sesión con {nombre}","bindAccount":"Vincular cuenta","scanQrCode":"Escanea el código QR con {nombre}","invalidThirdPartyParam":"Parámetros de terceros no válidos","account":"Cuenta","inputAccount":"Introduce tu cuenta","ldapLogin":"Inicio de sesión LDAP","resetPassword":"Restablecer contraseña","resetPasswordDesc":"Restablecer la contraseña del usuario {nombre}. Se generará una nueva contraseña después de restablecerla.","resetSuccess":"Reinicio efectuado","resetSuccessDesc":"Se ha restablecido la contraseña. La nueva contraseña es: {contraseña}","copyPassword":"Copiar contraseña","poweredByLowcoder":"Desarrollado por Lowcoder.cloud"},"preLoad":{"jsLibraryHelpText":"Añade Bibliotecas JavaScript a tu Aplicación Actual mediante Direcciones URL. lodash, day.js, uuid, numbro están Integradas en el Sistema para su Uso Inmediato. Las Bibliotecas JavaScript se Cargan Antes de Inicializar la Aplicación, Lo Que Puede Tener un Impacto en el Rendimiento de la Aplicación.","exportedAs":"Exportado como","urlTooltip":"Dirección URL de la biblioteca JavaScript, se recomienda [unpkg.com](https://unpkg.com/) o [jsdelivr.net](https://www.jsdelivr.com/)","recommended":"Recomendado","viewJSLibraryDocument":"Documento","jsLibraryURLError":"URL no válida","jsLibraryExist":"La biblioteca JavaScript ya existe","jsLibraryEmptyContent":"No se han añadido bibliotecas JavaScript","jsLibraryDownloadError":"Error de descarga de la biblioteca JavaScript","jsLibraryInstallSuccess":"Biblioteca JavaScript instalada correctamente","jsLibraryInstallFailed":"Fallo en la instalación de la biblioteca JavaScript","jsLibraryInstallFailedCloud":"Puede que la biblioteca no esté disponible en el Sandbox, [Documentación](https://docs.lowcoder.cloud/build-apps/write-javascript/use-third-party-libraries#manually-import-libraries)\\n{mensaje}","jsLibraryInstallFailedHost":"{mensaje}","add":"Añadir nuevo","jsHelpText":"Añade un Método o Variable Global a la Aplicación Actual.","cssHelpText":"Añadir Estilos a la Aplicación Actual. La Estructura DOM Puede Cambiar Mientras Itera el Sistema. Intenta Modificar los Estilos a Través de las Propiedades de los Componentes.","scriptsAndStyles":"Guiones y estilos","jsLibrary":"Biblioteca JavaScript"},"editorTutorials":{"component":"Componente","componentContent":"El Panel de Componentes Derecho te ofrece muchos Bloques de Aplicación (Componentes) ya hechos. Puedes arrastrarlos al lienzo para utilizarlos. También puedes crear tus propios componentes con unos pocos conocimientos de programación.","canvas":"Lienzo","canvasContent":"Construye tus aplicaciones en el Lienzo con un enfoque \"Lo que ves es lo que hay\". Sólo tienes que arrastrar y soltar componentes para diseñar tu diseño, y utilizar los atajos de teclado para una edición rápida como borrar, copiar y pegar. Una vez seleccionado un componente, puedes ajustar todos los detalles, desde el estilo y el diseño hasta la vinculación de datos y el comportamiento lógico. Además, disfruta de la ventaja añadida del diseño adaptable, que garantiza que tus aplicaciones se vean bien en cualquier dispositivo.","queryData":"Consulta de datos","queryDataContent":"Aquí puedes crear Consultas de Datos y Conectarte a tu MySQL, MongoDB, Redis, Airtable y muchas Otras Fuentes de Datos. Tras configurar la Consulta, haz clic en \"Ejecutar\" para obtener los Datos y continuar con el Tutorial.","compProperties":"Propiedades de los componentes"},"homeTutorials":{"createAppContent":"🎉 Bienvenido a {productName}, haz clic en \\'App\\' y empieza a crear tu primera aplicación.","createAppTitle":"Crear aplicación"},"history":{"layout":"\\'{0}\\' ajuste del diseño","upgrade":"Actualizar \\'{0}\\'","delete":"Borrar \\'{0}\\'","add":"Añade \"0\".","modify":"Modificar \\'{0}\\'","rename":"Cambia el nombre de \"{1}\" a \"{0}\".","recover":"Recuperar la versión \"2","recoverVersion":"Recuperar versión","andSoOn":"etc.","timeFormat":"MM DD a las hh:mm A","emptyHistory":"Sin antecedentes","currentVersionWithBracket":" (Actual)","currentVersion":"Versión actual","justNow":"Ahora mismo","history":"Historia"},"home":{"allApplications":"Todas las aplicaciones","allModules":"Todos los módulos","allFolders":"Todas las carpetas","modules":"Módulos","module":"Módulo","trash":"Basura","queryLibrary":"Biblioteca de consultas","datasource":"Fuentes de datos","selectDatasourceType":"Selecciona el tipo de fuente de datos","home":"Inicio | Área de Administración","all":"Todos","app":"Aplicación","navigation":"Navegación","navLayout":"Navegación por PC","navLayoutDesc":"Menú a la izquierda para facilitar la navegación por el escritorio.","mobileTabLayout":"Navegación móvil","mobileTabLayoutDesc":"Barra de navegación inferior para una navegación móvil fluida.","folders":"Carpetas","folder":"Carpeta","rootFolder":"Raíz","import":"Importa","export":"Exportar a JSON","inviteUser":"Invitar a miembros","createFolder":"Crear carpeta","createFolderSubTitle":"Nombre de la carpeta:","moveToFolder":"Mover a carpeta","moveToTrash":"Mover a la papelera","moveToFolderSubTitle":"Desplázate a:","folderName":"Nombre de la carpeta:","resCardSubTitle":"{tiempo} por {creador}","trashEmpty":"La papelera está vacía.","projectEmpty":"Aquí no hay nada.","projectEmptyCanAdd":"Todavía no tienes ninguna aplicación. Haz clic en Nueva para empezar.","name":"Nombre","type":"Tipo","creator":"Creado por","lastModified":"Última modificación","deleteTime":"Borrar hora","createTime":"Crear tiempo","datasourceName":"Nombre de la fuente de datos","databaseName":"Nombre de la base de datos","nameCheckMessage":"El nombre no puede estar vacío","deleteElementTitle":"Borrar permanentemente","moveToTrashSubTitle":"{tipo} {nombre} se moverá a la papelera.","deleteElementSubTitle":"Borrar {tipo} {nombre} permanentemente, no se puede recuperar.","deleteSuccessMsg":"Eliminado con éxito","deleteErrorMsg":"Error borrado","recoverSuccessMsg":"Recuperado con éxito","newDatasource":"Nueva fuente de datos","creating":"Crear...","chooseDataSourceType":"Elige el tipo de fuente de datos","folderAlreadyExists":"La carpeta ya existe","newNavLayout":"{nombredeusuario} de {nombre} ","newApp":"nuevo {nombre} de {nombre} de {usuario} ","importError":"Error de importación, {mensaje}","exportError":"Error de exportación, {mensaje}","importSuccess":"Éxito de la importación","fileUploadError":"Error de carga de archivos","fileFormatError":"Error de formato de archivo","groupWithSquareBrackets":"[Grupo] ","allPermissions":"Propietario","shareLink":"Comparte el enlace: ","copyLink":"Copiar enlace","appPublicMessage":"Haz pública la aplicación. Cualquiera puede verla.","modulePublicMessage":"Haz que el módulo sea público. Cualquiera puede verlo.","memberPermissionList":"Permisos de los miembros: ","orgName":"{orgName} admins","addMember":"Añadir miembros","addPermissionPlaceholder":"Introduce un nombre para buscar miembros","searchMemberOrGroup":"Buscar miembros o grupos: ","addPermissionErrorMessage":"Fallo al añadir permiso, {mensaje}","copyModalTitle":"Clonarlo","copyNameLabel":"{tipo} nombre","copyModalfolderLabel":"Añadir a la carpeta","copyNamePlaceholder":"Por favor, introduce un {tipo} de nombre","chooseNavType":"Elige el tipo de navegación","createNavigation":"Crear navegación"},"carousel":{"dotPosition":"Posición de los puntos de navegación","autoPlay":"Reproducción automática","showDots":"Mostrar puntos de navegación"},"npm":{"invalidNpmPackageName":"Nombre o URL de paquete npm no válidos.","pluginExisted":"Este plugin npm ya existía","compNotFound":"No se ha encontrado el componente {compName}.","addPluginModalTitle":"Añadir plugin desde un repositorio npm","pluginNameLabel":"URL o nombre del paquete npm","noCompText":"Sin componentes.","compsLoading":"Cargando...","removePluginBtnText":"Elimina","addPluginBtnText":"Añadir plugin npm"},"toggleButton":{"valueDesc":"El Valor por Defecto del Botón Alternar, Por Ejemplo: Falso","trueDefaultText":"Ocultar","falseDefaultText":"Mostrar","trueLabel":"Texto para Verdadero","falseLabel":"Texto para Falso","trueIconLabel":"Icono de Verdadero","falseIconLabel":"Icono de Falso","iconPosition":"Icono Posición","showText":"Mostrar texto","alignment":"Alineación","showBorder":"Mostrar borde"},"componentDoc":{"markdownDemoText":"**Lowcoder** | Crea aplicaciones de software para tu Empresa y tus Clientes con mínima experiencia en codificación. Lowcoder es la mejor alternativa a Retool, Appsmith o Tooljet.","demoText":"Lowcoder | Crea aplicaciones de software para tu Empresa y tus Clientes con una mínima experiencia en codificación. Lowcoder es la mejor alternativa a Retool, Appsmith o Tooljet.","submit":"Envía","style":"Estilo","danger":"Peligro","warning":"Advertencia","success":"Éxito","menu":"Menú","link":"Enlace","customAppearance":"Apariencia personalizada","search":"Busca en","pleaseInputNumber":"Introduce un número","mostValue":"Más valor","maxRating":"Clasificación máxima","notSelect":"No seleccionado","halfSelect":"Media selección","pleaseSelect":"Selecciona","title":"Título","content":"Contenido","componentNotFound":"El componente no existe","example":"Ejemplos","defaultMethodDesc":"Establecer el valor de la propiedad {nombre}","propertyUsage":"Puedes leer información relacionada con el componente accediendo a las propiedades del componente por su nombre en cualquier lugar donde puedas escribir JavaScript.","property":"Propiedades","propertyName":"Nombre de la propiedad","propertyType":"Tipo","propertyDesc":"Descripción","event":"Eventos","eventName":"Nombre del evento","eventDesc":"Descripción","mehtod":"Métodos","methodUsage":"Puedes interactuar con los componentes a través de sus métodos, y puedes llamarlos por su nombre en cualquier lugar donde puedas escribir JavaScript. O puedes llamarlos a través de la acción \"Componente de control\" de un evento.","methodName":"Nombre del método","methodDesc":"Descripción","showBorder":"Mostrar borde","haveTry":"Pruébalo tú mismo","settings":"Configurar","settingValues":"Valor de ajuste","defaultValue":"Valor por defecto","time":"Tiempo","date":"Fecha","noValue":"Ninguno","xAxisType":"Tipo de eje X","hAlignType":"Alineación horizontal","leftLeftAlign":"Alineación izquierda-izquierda","leftRightAlign":"Alineación izquierda-derecha","topLeftAlign":"Alineación superior izquierda","topRightAlign":"Alineación superior derecha","validation":"Validación","required":"Necesario","defaultStartDateValue":"Fecha de inicio por defecto","defaultEndDateValue":"Fecha de finalización por defecto","basicUsage":"Uso básico","basicDemoDescription":"Los siguientes ejemplos muestran el uso básico del componente.","noDefaultValue":"Sin valor por defecto","forbid":"Prohibido","placeholder":"Marcador de posición","pleaseInputPassword":"Introduce una contraseña","password":"Contraseña","textAlign":"Alineación del texto","length":"Longitud","top":"Arriba","pleaseInputName":"Introduce tu nombre","userName":"Nombre","fixed":"Fijo","responsive":"Respuesta","workCount":"Recuento de palabras","cascaderOptions":"Opciones de Cascader","pleaseSelectCity":"Selecciona una ciudad","advanced":"Avanzado","showClearIcon":"Mostrar icono Borrar","likedFruits":"Favoritos","option":"Opción","singleFileUpload":"Carga de un solo archivo","multiFileUpload":"Carga múltiple de archivos","folderUpload":"Cargar carpeta","multiFile":"Varios archivos","folder":"Carpeta","open":"Abre","favoriteFruits":"Frutas favoritas","pleaseSelectOneFruit":"Selecciona una fruta","notComplete":"No Completo","complete":"Completa","echart":"EChart","lineChart":"Gráfico lineal","basicLineChart":"Gráfico de líneas básico","lineChartType":"Tipo de gráfico de líneas","stackLineChart":"Línea apilada","areaLineChart":"Línea de área","scatterChart":"Gráfico de dispersión","scatterShape":"Forma de dispersión","scatterShapeCircle":"Círculo","scatterShapeRect":"Rectángulo","scatterShapeTri":"Triángulo","scatterShapeDiamond":"Diamante","scatterShapePin":"Chincheta","scatterShapeArrow":"Flecha","pieChart":"Gráfico circular","basicPieChart":"Gráfico circular básico","pieChatType":"Tipo de gráfico circular","pieChartTypeCircle":"Gráfico de donuts","pieChartTypeRose":"Gráfico de rosas","titleAlign":"Título Cargo","color":"Color","dashed":"Guiones","imADivider":"Soy una línea divisoria","tableSize":"Tamaño de la tabla","subMenuItem":"SubMenú {num}","menuItem":"Menú {num}","labelText":"Etiqueta","labelPosition":"Etiqueta - Posición","labelAlign":"Etiqueta - Alinear","optionsOptionType":"Método de configuración","styleBackgroundColor":"Color de fondo","styleBorderColor":"Color del borde","styleColor":"Color de fuente","selectionMode":"Modo de selección de filas","paginationSetting":"Configuración de la paginación","paginationShowSizeChanger":"Ayudar a los usuarios a modificar el número de entradas por página","paginationShowSizeChangerButton":"Mostrar botón de cambio de tamaño","paginationShowQuickJumper":"Mostrar saltador rápido","paginationHideOnSinglePage":"Ocultar cuando sólo hay una página","paginationPageSizeOptions":"Tamaño de página","chartConfigCompType":"Tipo de gráfico","xConfigType":"Tipo de eje X","loading":"Cargando","disabled":"Discapacitados","minLength":"Longitud mínima","maxLength":"Longitud máxima","showCount":"Mostrar recuento de palabras","autoHeight":"Altura","thousandsSeparator":"Separador de miles","precision":"Posiciones decimales","value":"Valor por defecto","formatter":"Formato","min":"Valor mínimo","max":"Valor máximo","step":"Tamaño del paso","start":"Hora de inicio","end":"Fin de los tiempos","allowHalf":"Permitir media selección","filetype":"Tipo de archivo","showUploadList":"Mostrar lista de cargas","uploadType":"Tipo de carga","allowClear":"Mostrar icono Borrar","minSize":"Tamaño mínimo del archivo","maxSize":"Tamaño máximo del archivo","maxFiles":"Número máximo de archivos cargados","format":"Formato","minDate":"Fecha mínima","maxDate":"Fecha máxima","minTime":"Tiempo mínimo","maxTime":"Tiempo máximo","text":"Texto","type":"Tipo","hideHeader":"Ocultar cabecera","hideBordered":"Ocultar borde","src":"URL de la imagen","showInfo":"Mostrar valor","mode":"Modo","onlyMenu":"Sólo Menú","horizontalAlignment":"Alineación horizontal","row":"Izquierda","column":"Arriba","leftAlign":"Alineación izquierda","rightAlign":"Alineación correcta","percent":"Porcentaje","fixedHeight":"Altura fija","auto":"Adaptativo","directory":"Carpeta","multiple":"Varios archivos","singleFile":"Archivo único","manual":"Manual","default":"Por defecto","small":"Pequeño","middle":"Medio","large":"Grande","single":"Individual","multi":"Múltiple","close":"Cerrar","ui":"Modo IU","line":"Gráfico lineal","scatter":"Gráfico de dispersión","pie":"Gráfico circular","basicLine":"Gráfico de líneas básico","stackedLine":"Gráfico de líneas apiladas","areaLine":"Área Mapa del área","basicPie":"Gráfico circular básico","doughnutPie":"Gráfico de donuts","rosePie":"Gráfico de rosas","category":"Categoría Eje","circle":"Círculo","rect":"Rectángulo","triangle":"Triángulo","diamond":"Diamante","pin":"Chincheta","arrow":"Flecha","left":"Izquierda","right":"A la derecha","center":"Centro","bottom":"Fondo","justify":"Justificar ambos extremos"},"playground":{"url":"https://app.lowcoder.cloud/playground/{compType}/1","data":"Estado actual de los datos","preview":"Vista previa","property":"Propiedades","console":"Consola Visual Script","executeMethods":"Ejecutar métodos","noMethods":"Sin métodos.","methodParams":"Parámetros del método","methodParamsHelp":"Parámetros del método de entrada utilizando JSON. Por ejemplo, puedes establecer los parámetros de setValue\\ con: [1] o 1"},"calendar":{"headerBtnBackground":"Botón Fondo","btnText":"Texto del botón","title":"Título","selectBackground":"Antecedentes seleccionados"},"componentDocExtra":{"table":"Documentación adicional para el componente Tabla"},"idSource":{"title":"Proveedores OAuth","form":"Envía un correo electrónico a","pay":"Premium","enable":"Activa","unEnable":"No activado","loginType":"Tipo de conexión","status":"Estado","desc":"Descripción","manual":"Agenda:","syncManual":"Sincronizar Agenda","syncManualSuccess":"Sincronización realizada","enableRegister":"Permitir la inscripción","saveBtn":"Guardar y Activar","save":"Guarda","none":"Ninguno","formPlaceholder":"Introduce {etiqueta}","formSelectPlaceholder":"Selecciona la {etiqueta}","saveSuccess":"Guardado correctamente","dangerLabel":"Zona de peligro","dangerTip":"Desactivar este proveedor de ID puede provocar que algunos usuarios no puedan iniciar sesión. Procede con precaución.","disable":"Desactiva","disableSuccess":"Desactivado correctamente","encryptedServer":"-------- Cifrado en el lado del servidor --------","disableTip":"Consejos","disableContent":"Desactivar este proveedor de ID puede provocar que algunos usuarios no puedan iniciar sesión. ¿Estás seguro de proceder?","manualTip":"","lockTip":"El Contenido está Bloqueado. Para realizar cambios, haz clic en el {icono} para desbloquearlo.","lockModalContent":"La modificación del campo \"Atributo ID\" puede tener repercusiones importantes en la identificación del usuario. Por favor, confirma que comprendes las implicaciones de este cambio antes de proceder.","payUserTag":"Premium"},"slotControl":{"configSlotView":"Configurar vista de ranura"},"jsonLottie":{"lottieJson":"Lottie JSON","speed":"Velocidad","width":"Anchura","height":"Altura","backgroundColor":"Color de fondo","animationStart":"Inicio de la animación","valueDesc":"Datos JSON actuales","loop":"Bucle","auto":"Auto","onHover":"Al pasar por encima","singlePlay":"Juego individual","endlessLoop":"Bucle sin fin","keepLastFrame":"Mantener visualizado el último fotograma"},"timeLine":{"titleColor":"Título Color","subTitleColor":"Color del subtítulo","labelColor":"Color de la etiqueta","value":"Datos cronológicos","mode":"Orden de visualización","left":"Contenido Correcto","right":"Contenido Izquierda","alternate":"Orden alternativo del contenido","modeTooltip":"Configura el contenido para que aparezca a izquierda/derecha o alternativamente en ambos lados de la línea de tiempo","reverse":"Eventos más recientes primero","pending":"Texto de nodo pendiente","pendingDescription":"Si se establece, se mostrará un último nodo con el texto y un indicador de espera.","defaultPending":"Mejora continua","clickTitleEvent":"Haz clic en el título Evento","clickTitleEventDesc":"Haz clic en el título Evento","Introduction":"Introducción Claves","helpTitle":"Título del cronograma (Obligatorio)","helpsubTitle":"Subtítulo del cronograma","helpLabel":"Etiqueta de la línea de tiempo, utilizada para mostrar las fechas","helpColor":"Indica el color del nodo de la línea de tiempo","helpDot":"Representación de los nodos de la línea de tiempo como iconos de diseño Ant","helpTitleColor":"Controlar individualmente el color del título del nodo","helpSubTitleColor":"Controlar Individualmente el Color del Subtítulo del Nodo","helpLabelColor":"Controlar Individualmente el Color del Icono del Nodo","valueDesc":"Datos de la cronología","clickedObjectDesc":"Datos del elemento pulsado","clickedIndexDesc":"Índice de elementos pulsados"},"comment":{"value":"Datos de la lista de comentarios","showSendButton":"Permitir comentarios","title":"Título","titledDefaultValue":"%d Comentario en total","placeholder":"Mayús + Intro para comentar; introduce @ o # para entrada rápida","placeholderDec":"Marcador de posición","buttonTextDec":"Título del botón","buttonText":"Comentario","mentionList":"Datos de la Lista de Menciones","mentionListDec":"Palabras clave de mención clave; Datos de la lista de mención de valor","userInfo":"Información del usuario","dateErr":"Error de fecha","commentList":"Lista de comentarios","deletedItem":"Elemento eliminado","submitedItem":"Artículo enviado","deleteAble":"Mostrar botón Eliminar","Introduction":"Introducción Claves","helpUser":"Información del usuario (Obligatorio)","helpname":"Nombre de usuario (Obligatorio)","helpavatar":"Avatar URL (Alta prioridad)","helpdisplayName":"Nombre para mostrar (prioridad baja)","helpvalue":"Contenido de los comentarios","helpcreatedAt":"Fecha de creación"},"mention":{"mentionList":"Datos de la Lista de Menciones"},"autoComplete":{"value":"Auto Complete Value","checkedValueFrom":"Valor comprobado Desde","ignoreCase":"Buscar Ignorar Caso","searchLabelOnly":"Buscar sólo etiqueta","searchFirstPY":"Buscar First Pinyin","searchCompletePY":"Buscar Pinyin completo","searchText":"Buscar texto","SectionDataName":"Autocompletar datos","valueInItems":"Valor en artículos","type":"Tipo","antDesign":"AntDesign","normal":"Normal","selectKey":"Clave","selectLable":"Etiqueta","ComponentType":"Tipo de componente","colorIcon":"Azul","grewIcon":"Gris","noneIcon":"Ninguno","small":"Pequeño","large":"Grande","componentSize":"Tamaño del componente","Introduction":"Introducción Claves","helpLabel":"Etiqueta","helpValue":"Valor"},"responsiveLayout":{"column":"Columnas","atLeastOneColumnError":"El diseño adaptable mantiene al menos una columna","columnsPerRow":"Columnas por fila","columnsSpacing":"Espacio entre columnas (px)","horizontal":"Horizontal","vertical":"Vertical","mobile":"Móvil","tablet":"Tableta","desktop":"Escritorio","rowStyle":"Estilo Fila","columnStyle":"Estilo columna","minWidth":"Mín. Anchura","rowBreak":"Rotura de fila","matchColumnsHeight":"Igualar altura de columnas","rowLayout":"Disposición de filas","columnsLayout":"Disposición de las columnas"},"navLayout":{"mode":"Modo","modeInline":"En línea","modeVertical":"Vertical","width":"Anchura","widthTooltip":"Píxel o Porcentaje, por ejemplo 520, 60%.","navStyle":"Estilo de menú","navItemStyle":"Estilo del elemento de menú"}} diff --git a/client/packages/lowcoder/src/i18n/locales/translation_files/fr.json b/client/packages/lowcoder/src/i18n/locales/translation_files/fr.json index 5196d3bd6..69dae28cf 100644 --- a/client/packages/lowcoder/src/i18n/locales/translation_files/fr.json +++ b/client/packages/lowcoder/src/i18n/locales/translation_files/fr.json @@ -1 +1 @@ -{"productName":"Lowcoder","productDesc":"Crée des applications logicielles pour ton entreprise et tes clients avec un minimum d'expérience en matière de codage. Lowcoder est une excellente alternative à Retool, Appsmith et Tooljet.","notSupportedBrowser":"Ton navigateur actuel peut avoir des problèmes de compatibilité. Pour une expérience utilisateur optimale, utilise la dernière version de Chrome.","create":"Créer","move":"Déplacer","addItem":"Ajouter","newItem":"Nouveau","copy":"Copie","rename":"Renommer","delete":"Supprimer","deletePermanently":"Supprimer définitivement","remove":"Enlever","recover":"Récupérer","edit":"Éditer","view":"Voir","value":"Valeur","data":"Données","information":"Informations","success":"Succès","warning":"Avertissement","error":"Erreur","reference":"Référence","text":"Texte","label":"Étiquette","color":"Couleur","form":"Formulaire","menu":"Menu","menuItem":"Point de menu","ok":"OK","cancel":"Annuler","finish":"Finir","reset":"Remise à zéro","icon":"Icône","code":"Code","title":"Titre","emptyContent":"Contenu vide","more":"Plus d'informations","search":"Recherche","back":"Retour","accessControl":"Contrôle d'accès","copySuccess":"Copié avec succès","copyError":"Erreur de copie","api":{"publishSuccess":"Publié avec succès","recoverFailed":"Échec de la récupération","needUpdate":"Ta version actuelle est obsolète. Mets-toi à jour avec la dernière version."},"codeEditor":{"notSupportAutoFormat":"L'éditeur de code actuel ne prend pas en charge le formatage automatique.","fold":"Plier"},"exportMethod":{"setDesc":"Définir la propriété : {propriété}","clearDesc":"Efface la propriété : {propriété}","resetDesc":"Réinitialiser la propriété : {propriété} à la valeur par défaut"},"method":{"focus":"Définir l'objectif","focusOptions":"Options de mise au point. Voir HTMLElement.focus()","blur":"Enlever la mise au point","click":"Clique sur","select":"Sélectionner tout le texte","setSelectionRange":"Définir les positions de début et de fin de la sélection de texte","selectionStart":"Index basé sur 0 du premier caractère sélectionné","selectionEnd":"Index basé sur 0 du caractère après le dernier caractère sélectionné","setRangeText":"Remplacer la plage de texte","replacement":"Chaîne à insérer","replaceStart":"Index basé sur 0 du premier caractère à remplacer","replaceEnd":"Index basé sur 0 du caractère après le dernier caractère à remplacer"},"errorBoundary":{"encounterError":"Le chargement du composant a échoué. Vérifie ta configuration.","clickToReload":"Cliquer pour recharger","errorMsg":"Erreur : "},"imgUpload":{"notSupportError":"Prend en charge uniquement les types d'images {types}","exceedSizeError":"La taille de l'image ne doit pas dépasser {taille}"},"gridCompOperator":{"notSupport":"Non pris en charge","selectAtLeastOneComponent":"Choisis au moins une composante","selectCompFirst":"Sélectionne les composants avant de les copier","noContainerSelected":"[Bug] Aucun conteneur n'est sélectionné","deleteCompsSuccess":"Supprimé avec succès. Appuie sur la touche {undoKey} pour annuler.","deleteCompsTitle":"Supprimer des composants","deleteCompsBody":"Es-tu sûr de vouloir supprimer les composants sélectionnés {compNum} ?","cutCompsSuccess":"Coupe avec succès. Appuie sur {pasteKey} pour coller, ou sur {undoKey} pour annuler."},"leftPanel":{"queries":"Requêtes de données dans ton application","globals":"Variables de données globales","propTipsArr":"Éléments {num}","propTips":"{num} Clés","propTipArr":"Item {num}","propTip":"{num} Clé","stateTab":"État","settingsTab":"Réglages","toolbarTitle":"L'individualisation","toolbarPreload":"Scripts et styles","components":"Composants actifs","modals":"Modaux in-App","expandTip":"Cliquer pour développer les données de {composant}\\N","collapseTip":"Cliquer pour réduire les données de {composant}\\N"},"bottomPanel":{"title":"Requêtes de données","run":"Exécuter","noSelectedQuery":"Aucune requête sélectionnée","metaData":"Métadonnées de la source de données","noMetadata":"Pas de métadonnées disponibles","metaSearchPlaceholder":"Recherche de métadonnées","allData":"Toutes les tables"},"rightPanel":{"propertyTab":"Propriétés","noSelectedComps":"Aucun composant n'est sélectionné. Clique sur un composant pour afficher ses propriétés.","createTab":"Insérer","searchPlaceHolder":"Rechercher des composants ou des modules","uiComponentTab":"Composants","extensionTab":"Extensions","modulesTab":"Modules","moduleListTitle":"Modules","pluginListTitle":"Plugins","emptyModules":"Les modules sont des applications Mikro-Apps réutilisables. Tu peux les intégrer dans ton application.","searchNotFound":"Tu ne trouves pas le bon composant ? Soumettre un problème","emptyPlugins":"Aucun plugin n'a été ajouté","contactUs":"Nous contacter","issueHere":"ici."},"prop":{"expand":"Élargir","columns":"Colonnes","videokey":"Clé vidéo","rowSelection":"Sélection des rangs","toolbar":"Barre d'outils","pagination":"Pagination","logo":"Logo","style":"Style","inputs":"Entrées","meta":"Métadonnées","data":"Données","hide":"Cache-toi","loading":"Chargement","disabled":"Désactivé","placeholder":"Placeholder","showClear":"Afficher le bouton d'effacement","showSearch":"Recherche possible","defaultValue":"Valeur par défaut","required":"Champ obligatoire","readOnly":"Lecture seule","readOnlyTooltip":"Les composants en lecture seule apparaissent normaux mais ne peuvent pas être modifiés.","minimum":"Minimum","maximum":"Maximum","regex":"Regex","minLength":"Longueur minimale","maxLength":"Longueur maximale","height":"Hauteur","width":"Largeur","selectApp":"Sélectionne l'application","showCount":"Afficher le décompte","textType":"Type de texte","customRule":"Règle personnalisée","customRuleTooltip":"Une chaîne non vide indique une erreur ; une chaîne vide ou nulle signifie que la validation est réussie. Exemple : ","manual":"Manuel","map":"Carte","json":"JSON","use12Hours":"Utilise le format 12 heures","hourStep":"Heure Étape","minuteStep":"Pas de minute","secondStep":"Deuxième étape","minDate":"Date minimum","maxDate":"Date maximale","minTime":"Durée minimale","maxTime":"Durée maximale","type":"Type","showLabel":"Afficher l'étiquette","showHeader":"Afficher l'en-tête","showBody":"Montrer le corps","showFooter":"Afficher le pied de page","maskClosable":"Clique sur Extérieur pour fermer","showMask":"Afficher le masque"},"autoHeightProp":{"auto":"Auto","fixed":"Fixe"},"labelProp":{"text":"Étiquette","tooltip":"Info-bulle","position":"Position","left":"Gauche","top":"Haut","align":"Alignement","width":"Largeur","widthTooltip":"La largeur de l'étiquette prend en charge les pourcentages (%) et les pixels (px)."},"eventHandler":{"eventHandlers":"Gestionnaires d'événements","emptyEventHandlers":"Pas de gestionnaire d'événements","incomplete":"Sélection incomplète","inlineEventTitle":"Sur {nom de l'événement}","event":"Événement","action":"Action","noSelect":"Pas de sélection","runQuery":"Exécuter une requête de données","selectQuery":"Sélectionner une requête de données","controlComp":"Contrôler un composant","runScript":"Exécuter JavaScript","runScriptPlaceHolder":"Inscris le code ici","component":"Composant","method":"Méthode","setTempState":"Définir une valeur d'état temporaire","state":"État","triggerModuleEvent":"Déclencher un événement de module","moduleEvent":"Événement du module","goToApp":"Aller à une autre application","queryParams":"Paramètres de la requête","hashParams":"Paramètres de hachage","showNotification":"Afficher une notification","text":"Texte","level":"Niveau","duration":"Durée","notifyDurationTooltip":"L'unité de temps peut être 's' (seconde, par défaut) ou 'ms' (milliseconde). La durée maximale est de {max} secondes","goToURL":"Ouvrir une URL","openInNewTab":"Ouvrir dans un nouvel onglet","copyToClipboard":"Copier une valeur dans le presse-papiers","copyToClipboardValue":"Valeur","export":"Exportation de données","exportNoFileType":"Pas de sélection (optionnel)","fileName":"Nom du fichier","fileNameTooltip":"Inclure l'extension pour spécifier le type de fichier, par exemple, \\N \"image.png\\\".","fileType":"Type de fichier","condition":"Ne cours que lorsque...","conditionTooltip":"Ne lance le gestionnaire d'événement que lorsque cette condition est évaluée à 'vrai'.","debounce":"Debounce pour","throttle":"L'accélérateur pour","slowdownTooltip":"Utilise la fonction debounce ou throttle pour contrôler la fréquence des déclenchements d'action. L'unité de temps peut être \\'ms\\' (milliseconde, par défaut) ou \\'s\\' (seconde).","notHandledError":"Non traité","currentApp":"Actuel"},"event":{"submit":"Soumettre","submitDesc":"Déclencheurs à la soumission","change":"Changer","changeDesc":"Déclencheurs sur les changements de valeur","focus":"Focus","focusDesc":"Déclencheurs sur la focalisation","blur":"Flou","blurDesc":"Déclencheurs sur le flou","click":"Clique sur","clickDesc":"Déclencheurs au clic","close":"Fermer","closeDesc":"Déclencheurs à la fermeture","parse":"Analyser","parseDesc":"Déclencheurs sur Parse","success":"Succès","successDesc":"Déclencheurs de succès","delete":"Supprimer","deleteDesc":"Déclencheurs de suppression","mention":"Mention","mentionDesc":"Déclencheurs à la mention"},"themeDetail":{"primary":"Couleur de la marque","primaryDesc":"Couleur primaire par défaut utilisée par la plupart des composants.","textDark":"Couleur foncée du texte","textDarkDesc":"Utilisé lorsque la couleur d'arrière-plan est claire","textLight":"Couleur de texte claire","textLightDesc":"Utilisé lorsque la couleur d'arrière-plan est sombre","canvas":"Couleur de la toile","canvasDesc":"Couleur d'arrière-plan par défaut de l'application","primarySurface":"Couleur du conteneur","primarySurfaceDesc":"Couleur d'arrière-plan par défaut pour les composants tels que les tableaux","borderRadius":"Rayon de la bordure","borderRadiusDesc":"Rayon de bordure par défaut utilisé par la plupart des composants","chart":"Style de graphique","chartDesc":"Entrée pour Echarts","echartsJson":"Thème JSON","margin":"Marge","marginDesc":"Marge par défaut généralement utilisée pour la plupart des composants.","padding":"Rembourrage","paddingDesc":"Rembourrage par défaut généralement utilisé pour la plupart des composants.","containerheaderpadding":"Rembourrage de l'en-tête","containerheaderpaddingDesc":"Rembourrage d'en-tête par défaut généralement utilisé pour la plupart des composants.","gridColumns":"Colonnes de la grille","gridColumnsDesc":"Nombre de colonnes par défaut généralement utilisé pour la plupart des conteneurs."},"style":{"resetTooltip":"Réinitialiser les styles. Efface le champ de saisie pour réinitialiser un style individuel.","textColor":"Couleur du texte","contrastText":"Contraste Couleur du texte","generated":"Généré","customize":"Personnaliser","staticText":"Texte statique","accent":"Accent","validate":"Message de validation","border":"Couleur de la bordure","borderRadius":"Rayon de la bordure","borderWidth":"Largeur de la bordure","background":"Contexte","headerBackground":"Arrière-plan de l'en-tête","footerBackground":"Arrière-plan du pied de page","fill":"Remplir","track":"Poursuivre","links":"Liens","thumb":"Pouce","thumbBorder":"Bordure du pouce","checked":"Vérifié","unchecked":"Non vérifié","handle":"Poignée","tags":"Tags","tagsText":"Texte des étiquettes","multiIcon":"Icône multi-sélection","tabText":"Texte de l'onglet","tabAccent":"Onglet Accent","checkedBackground":"Arrière-plan vérifié","uncheckedBackground":"Arrière-plan non vérifié","uncheckedBorder":"Bordure non vérifiée","indicatorBackground":"Contexte de l'indicateur","tableCellText":"Texte de la cellule","selectedRowBackground":"Arrière-plan de la rangée sélectionnée","hoverRowBackground":"Arrière-plan de la rangée de survol","alternateRowBackground":"Autre arrière-plan de rangée","tableHeaderBackground":"Arrière-plan de l'en-tête","tableHeaderText":"Texte de l'en-tête","toolbarBackground":"Arrière-plan de la barre d'outils","toolbarText":"Texte de la barre d'outils","pen":"Stylo","footerIcon":"Icône de bas de page","tips":"Conseils","margin":"Marge","padding":"Rembourrage","marginLeft":"Marge gauche","marginRight":"Marge droite","marginTop":"Marge supérieure","marginBottom":"Marge inférieure","containerheaderpadding":"Rembourrage de l'en-tête","containerfooterpadding":"Remplissage du pied de page","containerbodypadding":"Rembourrage du corps","minWidth":"Largeur minimale","aspectRatio":"Rapport d'aspect","textSize":"Taille du texte"},"export":{"hiddenDesc":"Si c'est vrai, le composant est caché","disabledDesc":"Si c'est vrai, le composant est désactivé et non interactif","visibleDesc":"Si c'est vrai, le composant est visible","inputValueDesc":"Valeur actuelle de l'entrée","invalidDesc":"Indique si la valeur est invalide","placeholderDesc":"Texte de remplacement lorsqu'aucune valeur n'est définie","requiredDesc":"Si c'est vrai, une valeur valide est requise","submitDesc":"Soumettre le formulaire","richTextEditorValueDesc":"Valeur actuelle de l'éditeur","richTextEditorReadOnlyDesc":"Si c'est vrai, l'éditeur est en lecture seule","richTextEditorHideToolBarDesc":"Si c'est vrai, la barre d'outils est cachée","jsonEditorDesc":"Données JSON actuelles","sliderValueDesc":"Valeur actuellement sélectionnée","sliderMaxValueDesc":"Valeur maximale du curseur","sliderMinValueDesc":"Valeur minimale du curseur","sliderStartDesc":"Valeur du point de départ sélectionné","sliderEndDesc":"Valeur du point final sélectionné","ratingValueDesc":"Cote actuellement sélectionnée","ratingMaxDesc":"Valeur nominale maximale","datePickerValueDesc":"Date actuellement sélectionnée","datePickerFormattedValueDesc":"Date sélectionnée formatée","datePickerTimestampDesc":"Horodatage de la date sélectionnée","dateRangeStartDesc":"Date de début de l'intervalle","dateRangeEndDesc":"Date de fin de l'intervalle","dateRangeStartTimestampDesc":"Horodatage de la date de début","dateRangeEndTimestampDesc":"Horodatage de la date de fin","dateRangeFormattedValueDesc":"Plage de dates formatée","dateRangeFormattedStartValueDesc":"Date de début formatée","dateRangeFormattedEndValueDesc":"Date de fin formatée","timePickerValueDesc":"Heure actuellement sélectionnée","timePickerFormattedValueDesc":"Formaté l'heure sélectionnée","timeRangeStartDesc":"Heure de début de la plage","timeRangeEndDesc":"Heure de fin de la plage","timeRangeFormattedValueDesc":"Formatage de l'intervalle de temps","timeRangeFormattedStartValueDesc":"Heure de début formatée","timeRangeFormattedEndValueDesc":"Heure de fin formatée"},"validationDesc":{"email":"Saisis une adresse électronique valide","url":"Saisis une URL valide","regex":"Tu dois correspondre au modèle spécifié","maxLength":"Trop de caractères, actuel : {longueur}, maximum : {maxLength}","minLength":"Pas assez de caractères, actuel : {longueur}, minimum : {minLength}","maxValue":"La valeur dépasse le maximum, courant : {valeur}, maximum : {max}","minValue":"Valeur inférieure au minimum, courant : {valeur}, minimum : {min}","maxTime":"Le temps dépasse le maximum, actuel : {heure}, maximum : {maxTime}","minTime":"Temps inférieur au minimum, actuel : {time}, minimum : {minTime}","maxDate":"La date dépasse le maximum, courant : {date}, maximum : {maxDate}","minDate":"Date inférieure au minimum, courant : {date}, minimum : {minDate}"},"query":{"noQueries":"Aucune requête de données n'est disponible.","queryTutorialButton":"Voir les documents {valeur}","datasource":"Tes sources de données","newDatasource":"Nouvelle source de données","generalTab":"Généralités","notificationTab":"Notification","advancedTab":"Avancé","showFailNotification":"Afficher une notification en cas d'échec","failCondition":"Conditions de défaillance","failConditionTooltip1":"Personnalise les conditions de défaillance et les notifications correspondantes.","failConditionTooltip2":"Si l'une des conditions revient vraie, la requête est marquée comme ayant échoué et déclenche la notification correspondante.","showSuccessNotification":"Afficher la notification en cas de succès","successMessageLabel":"Message de réussite","successMessage":"Exécution réussie","notifyDuration":"Durée","notifyDurationTooltip":"Durée de la notification. L'unité de temps peut être \\Ns (seconde, par défaut) ou \\Nms (milliseconde). La valeur par défaut est {default}s. La valeur maximale est {max}s.","successMessageWithName":"{nom} exécution réussie","failMessageWithName":"L'exécution de {nom} a échoué : {résultat}","showConfirmationModal":"Afficher la fenêtre de confirmation avant l'exécution","confirmationMessageLabel":"Message de confirmation","confirmationMessage":"Es-tu sûr de vouloir exécuter cette requête de données ?","newQuery":"Nouvelle requête de données","newFolder":"Nouveau dossier","recentlyUsed":"Récemment utilisé","folder":"Dossier","folderNotEmpty":"Le dossier n'est pas vide","dataResponder":"Répondant aux données","tempState":"État temporaire","transformer":"Transformateur","quickRestAPI":"Requête REST","quickStreamAPI":"Requête de flux","quickGraphql":"Requête GraphQL","lowcoderAPI":"API Lowcoder","executeJSCode":"Exécuter le code JavaScript","importFromQueryLibrary":"Importer à partir de la bibliothèque de requêtes","importFromFile":"Importer à partir d'un fichier","triggerType":"Déclenché lorsque...","triggerTypeAuto":"Les entrées changent ou au chargement de la page","triggerTypePageLoad":"Lorsque l'application (la page) se charge","triggerTypeManual":"Seulement lorsque tu le déclenches manuellement","chooseDataSource":"Choisis la source de données","method":"Méthode","updateExceptionDataSourceTitle":"Mettre à jour une source de données défaillante","updateExceptionDataSourceContent":"Mets à jour la requête suivante avec la même source de données défaillante :","update":"Mise à jour","disablePreparedStatement":"Désactiver les déclarations préparées","disablePreparedStatementTooltip":"La désactivation des instructions préparées permet de générer du SQL dynamique, mais augmente le risque d'injection SQL","timeout":"Délai d'attente après","timeoutTooltip":"Unité par défaut : ms. Unités d'entrée prises en charge : ms, s. Valeur par défaut : {defaultSeconds} secondes. Valeur maximale : {maxSeconds} secondes. Par exemple, 300 (c'est-à-dire 300 ms), 800 ms, 5 s.","periodic":"Exécute périodiquement cette requête de données","periodicTime":"Période","periodicTimeTooltip":"Période entre deux exécutions successives. Unité par défaut : ms. Unités d'entrée prises en charge : ms, s. Valeur minimale : 100 ms. L'exécution périodique est désactivée pour les valeurs inférieures à 100ms. Par exemple, 300 (c'est-à-dire 300ms), 800ms, 5s.","cancelPrevious":"Ignorer les résultats des exécutions précédentes non terminées","cancelPreviousTooltip":"Si une nouvelle exécution est déclenchée, le résultat des exécutions précédentes non terminées sera ignoré si elles ne se sont pas terminées, et ces exécutions ignorées ne déclencheront pas la liste d'événements de la requête.","dataSourceStatusError":"Si une nouvelle exécution est déclenchée, le résultat des exécutions précédentes non terminées sera ignoré, et les exécutions ignorées ne déclencheront pas la liste d'événements de la requête.","success":"Succès","fail":"Échec","successDesc":"Déclenché lorsque l'exécution est réussie","failDesc":"Déclenché lorsque l'exécution échoue","fixedDelayError":"La requête n'a pas été exécutée","execSuccess":"Exécution réussie","execFail":"Échec de l'exécution","execIgnored":"Les résultats de cette requête ont été ignorés","deleteSuccessMessage":"Supprimé avec succès. Tu peux utiliser {undoKey} pour annuler","dataExportDesc":"Données obtenues par la requête actuelle","codeExportDesc":"Code d'état de la requête en cours","successExportDesc":"Si la requête actuelle a été exécutée avec succès","messageExportDesc":"Informations renvoyées par la requête en cours","extraExportDesc":"Autres données dans la requête actuelle","isFetchingExportDesc":"La requête actuelle est-elle dans la demande ?","runTimeExportDesc":"Temps d'exécution de la requête actuelle (ms)","latestEndTimeExportDesc":"Dernier temps d'exécution","triggerTypeExportDesc":"Type de déclencheur","chooseResource":"Choisis une ressource","createDataSource":"Créer une nouvelle source de données","editDataSource":"Éditer","datasourceName":"Nom","datasourceNameRuleMessage":"Saisis le nom de la source de données","generalSetting":"Paramètres généraux","advancedSetting":"Paramètres avancés","port":"Port","portRequiredMessage":"Saisis un port","portErrorMessage":"Saisis un port correct","connectionType":"Type de connexion","regular":"Régulière","host":"Hôte","hostRequiredMessage":"Saisis le nom de domaine ou l'adresse IP de l'hôte","userName":"Nom de l'utilisateur","password":"Mot de passe","encryptedServer":"-------- Crypté du côté du serveur --------","uriRequiredMessage":"Saisis un URI","urlRequiredMessage":"Saisis une URL","uriErrorMessage":"Saisis un URI correct","urlErrorMessage":"Saisis une URL correcte","httpRequiredMessage":"Saisis http:// ou https://","databaseName":"Nom de la base de données","databaseNameRequiredMessage":"Saisis un nom de base de données","useSSL":"Utiliser SSL","userNameRequiredMessage":"Saisis ton nom","passwordRequiredMessage":"Saisis ton mot de passe","authentication":"Authentification","authenticationType":"Type d'authentification","sslCertVerificationType":"Vérification des certificats SSL","sslCertVerificationTypeDefault":"Vérifier le certificat de l'autorité de certification","sslCertVerificationTypeSelf":"Vérifier le certificat auto-signé","sslCertVerificationTypeDisabled":"Désactivé","selfSignedCert":"Cert auto-signé","selfSignedCertRequireMsg":"Saisis ton certificat","enableTurnOffPreparedStatement":"Activer le basculement des instructions préparées pour les requêtes","enableTurnOffPreparedStatementTooltip":"Tu peux activer ou désactiver les instructions préparées dans l'onglet Avancé de la requête.","serviceName":"Nom du service","serviceNameRequiredMessage":"Saisis le nom de ton service","useSID":"Utiliser le SID","connectSuccessfully":"Connexion réussie","saveSuccessfully":"Sauvegardé avec succès","database":"Base de données","cloudHosting":"Lowcoder hébergé dans le nuage ne peut pas accéder aux services locaux en utilisant 127.0.0.1 ou localhost. Essaie de te connecter aux sources de données du réseau public ou utilise un proxy inverse pour les services privés.","notCloudHosting":"Pour le déploiement hébergé par docker, Lowcoder utilise des réseaux en pont, donc 127.0.0.1 et localhost ne sont pas valides pour les adresses d'hôtes. Pour accéder aux sources de données des machines locales, réfère-toi à","howToAccessHostDocLink":"Comment accéder à l'API/DB de l'hôte","returnList":"Retourner","chooseDatasourceType":"Choisis le type de source de données","viewDocuments":"Voir les documents","testConnection":"Connexion de test","save":"Sauvegarde","whitelist":"Liste d'admissibilité","whitelistTooltip":"Ajoute les adresses IP de Lowcoder\\ à la liste d'autorisation de ta source de données si nécessaire.","address":"Adresse : ","nameExists":"Le nom {nom} existe déjà","jsQueryDocLink":"A propos de JavaScript Query","dynamicDataSourceConfigLoadingText":"Chargement de la configuration de la source de données supplémentaire...","dynamicDataSourceConfigErrText":"Échec du chargement de la configuration de la source de données supplémentaire.","retry":"Réessayer"},"sqlQuery":{"keyValuePairs":"Paires clé-valeur","object":"Objet","allowMultiModify":"Permettre la modification de plusieurs rangs","allowMultiModifyTooltip":"Si cette option est sélectionnée, toutes les lignes qui remplissent les conditions sont prises en compte. Sinon, seule la première ligne remplissant les conditions est prise en compte.","array":"Array","insertList":"Liste d'insertion","insertListTooltip":"Valeurs insérées alors qu'elles n'existent pas","filterRule":"Règle de filtrage","updateList":"Liste des mises à jour","updateListTooltip":"Les valeurs mises à jour au fur et à mesure peuvent être remplacées par les mêmes valeurs de la liste d'insertion.","sqlMode":"Mode SQL","guiMode":"Mode GUI","operation":"Fonctionnement","insert":"Insérer","upsert":"Insérer, mais mettre à jour en cas de conflit","update":"Mise à jour","delete":"Supprimer","bulkInsert":"Insertion en vrac","bulkUpdate":"Mise à jour en vrac","table":"Tableau","primaryKeyColumn":"Colonne de clé primaire"},"EsQuery":{"rawCommand":"Commande brute","queryTutorialButton":"Voir les documents de l'API Elasticsearch","request":"Demande"},"googleSheets":{"rowIndex":"Index des rangs","spreadsheetId":"ID de la feuille de calcul","sheetName":"Nom de la feuille","readData":"Lire les données","appendData":"Ajouter une ligne","updateData":"Rangée de mise à jour","deleteData":"Supprimer une ligne","clearData":"Effacer la rangée","serviceAccountRequireMessage":"Saisis ton compte de service","ASC":"ASC","DESC":"DESC","sort":"Trier","sortPlaceholder":"Nom"},"queryLibrary":{"export":"Exporter vers JSON","noInput":"La requête actuelle n'a pas d'entrée","inputName":"Nom","inputDesc":"Description","emptyInputs":"Pas d'entrées","clickToAdd":"Ajouter","chooseQuery":"Choisis la requête","viewQuery":"Voir la requête","chooseVersion":"Choisis la version","latest":"Dernières nouvelles","publish":"Publie","historyVersion":"Version historique","deleteQueryLabel":"Supprimer une requête","deleteQueryContent":"La requête ne peut pas être récupérée après sa suppression. Effacer la requête ?","run":"Exécuter","readOnly":"Lecture seule","exit":"Sortie","recoverAppSnapshotContent":"Restaure la requête actuelle à la version {version}","searchPlaceholder":"Recherche","allQuery":"Toutes les requêtes","deleteQueryTitle":"Supprimer une requête","unnamed":"Sans nom","publishNewVersion":"Publie une nouvelle version","publishSuccess":"Publié avec succès","version":"Version","desc":"Description"},"snowflake":{"accountIdentifierTooltip":"Voir ","extParamsTooltip":"Configurer des paramètres de connexion supplémentaires"},"lowcoderQuery":{"queryOrgUsers":"Interroger les utilisateurs de l'espace de travail"},"redisQuery":{"rawCommand":"Commande brute","command":"Commande","queryTutorial":"Voir les documents sur les commandes Redis"},"httpQuery":{"bodyFormDataTooltip":"Si {type} est sélectionné, le format de la valeur doit être {objet}. Exemple : {exemple}","text":"Texte","file":"Fichier","extraBodyTooltip":"Les valeurs clés dans le corps supplémentaire seront ajoutées au corps avec des types de données JSON ou Form.","forwardCookies":"Cookies en avant","forwardAllCookies":"Transmettre tous les cookies"},"smtpQuery":{"attachment":"Pièce jointe","attachmentTooltip":"Peut être utilisé avec le composant de téléchargement de fichiers, les données doivent être converties en : ","MIMETypeUrl":"https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types","sender":"Expéditeur","recipient":"Récipiendaire","carbonCopy":"Copie carbone","blindCarbonCopy":"Copie carbone aveugle","subject":"Sujet","content":"Contenu","contentTooltip":"Prend en charge la saisie de texte ou de HTML"},"uiCompCategory":{"dashboards":"Tableaux de bord et rapports","layout":"Mise en page et navigation","forms":"Collecte de données et formulaires","collaboration":"Réunion et collaboration","projectmanagement":"Gestion de projet","scheduling":"Calendrier et programmation","documents":"Gestion des documents et des dossiers","itemHandling":"Traitement des articles et des signatures","multimedia":"Multimédia et animation","integration":"Intégration et extension"},"uiComp":{"autoCompleteCompName":"Auto Complete","autoCompleteCompDesc":"Un champ de saisie qui fournit des suggestions au fur et à mesure que tu tapes, ce qui améliore l'expérience de l'utilisateur et la précision.","autoCompleteCompKeywords":"suggestions, autocomplétion, saisie, entrée","inputCompName":"Entrée","inputCompDesc":"Un champ de saisie de texte de base permettant aux utilisateurs de saisir et de modifier du texte.","inputCompKeywords":"texte, entrée, champ, modifier","textAreaCompName":"Zone de texte","textAreaCompDesc":"Une entrée de texte sur plusieurs lignes pour les contenus plus longs, tels que les commentaires ou les descriptions.","textAreaCompKeywords":"multiligne, textarea, entrée, texte","passwordCompName":"Mot de passe","passwordCompDesc":"Un champ sécurisé pour la saisie du mot de passe, masquant les caractères pour plus de confidentialité.","passwordCompKeywords":"mot de passe, sécurité, entrée, caché","richTextEditorCompName":"Éditeur de texte enrichi","richTextEditorCompDesc":"Un éditeur de texte avancé prenant en charge des options de formatage riches comme le gras, l'italique et les listes.","richTextEditorCompKeywords":"éditeur, texte, formatage, contenu riche","numberInputCompName":"Numéro Entrée","numberInputCompDesc":"Un champ spécifique pour la saisie numérique, avec des commandes pour incrémenter et décrémenter les valeurs.","numberInputCompKeywords":"nombre, entrée, incrémenter, décrémenter","sliderCompName":"Coulisses","sliderCompDesc":"Un composant graphique à curseur pour sélectionner une valeur ou une plage dans une échelle définie.","sliderCompKeywords":"curseur, plage, entrée, graphique","rangeSliderCompName":"Curseur de gamme","rangeSliderCompDesc":"Un curseur à deux poignées pour sélectionner une plage de valeurs, utile pour filtrer ou fixer des limites.","rangeSliderCompKeywords":"gamme, curseur, double poignée, filtre","ratingCompName":"Evaluation","ratingCompDesc":"Un composant permettant de saisir les évaluations des utilisateurs, affichées sous forme d'étoiles.","ratingCompKeywords":"évaluation, étoiles, retour d'information, commentaires","switchCompName":"Interrupteur","switchCompDesc":"Un interrupteur à bascule pour les décisions de type on/off ou oui/non.","switchCompKeywords":"interrupteur, interrupteur à bascule, on/off, contrôle","selectCompName":"Sélectionne","selectCompDesc":"Un menu déroulant permettant de choisir parmi une liste d'options.","selectCompKeywords":"menu déroulant, sélectionner, options, menu","multiSelectCompName":"Multiselect","multiSelectCompDesc":"Un composant qui permet de sélectionner plusieurs éléments dans une liste déroulante.","multiSelectCompKeywords":"multiselect, multiple, dropdown, choices","cascaderCompName":"Cascadeur","cascaderCompDesc":"Une liste déroulante à plusieurs niveaux pour la sélection de données hiérarchiques, comme la sélection d'un lieu.","cascaderCompKeywords":"cascader, hiérarchique, dropdown, niveaux","checkboxCompName":"Case à cocher","checkboxCompDesc":"Une case à cocher standard pour les options qui peuvent être sélectionnées ou désélectionnées.","checkboxCompKeywords":"case à cocher, options, sélectionner, basculer","radioCompName":"Radio","radioCompDesc":"Boutons radio permettant de sélectionner une option parmi un ensemble, lorsqu'un seul choix est autorisé.","radioCompKeywords":"radio, boutons, sélection, choix unique","segmentedControlCompName":"Contrôle segmenté","segmentedControlCompDesc":"Un contrôle avec des options segmentées pour basculer rapidement entre plusieurs choix.","segmentedControlCompKeywords":"segmenté, contrôle, bascule, options","fileUploadCompName":"Téléchargement de fichiers","fileUploadCompDesc":"Un composant pour le téléchargement de fichiers, avec prise en charge du glisser-déposer et de la sélection de fichiers.","fileUploadCompKeywords":"fichier, télécharger, glisser-déposer, sélectionner","dateCompName":"Date","dateCompDesc":"Un composant de sélection de date pour sélectionner des dates dans une interface de calendrier.","dateCompKeywords":"date, choisir, calendrier, sélectionner","dateRangeCompName":"Plage de dates","dateRangeCompDesc":"Un composant permettant de sélectionner une plage de dates, utile pour les systèmes de réservation ou les filtres.","dateRangeCompKeywords":"daterange, sélectionner, réserver, filtrer","timeCompName":"L'heure","timeCompDesc":"Un composant de sélection de l'heure pour choisir des heures spécifiques de la journée.","timeCompKeywords":"heure, choisir, sélectionner, horloge","timeRangeCompName":"Plage de temps","timeRangeCompDesc":"Un composant permettant de sélectionner une plage de temps, souvent utilisé dans les applications de planification.","timeRangeCompKeywords":"timerange, select, scheduling, duration","buttonCompName":"Bouton de formulaire","buttonCompDesc":"Un composant de bouton polyvalent pour soumettre des formulaires, déclencher des actions ou naviguer.","buttonCompKeywords":"bouton, soumettre, action, naviguer","linkCompName":"Lien","linkCompDesc":"Un composant d'affichage d'hyperliens pour la navigation ou la création de liens vers des ressources externes.","linkCompKeywords":"lien, hyperlien, navigation, externe","scannerCompName":"Scanner","scannerCompDesc":"Un composant pour scanner les codes-barres, les codes QR et d'autres données similaires.","scannerCompKeywords":"scanner, code-barres, code QR, scanner","dropdownCompName":"Liste déroulante","dropdownCompDesc":"Un menu déroulant pour afficher de façon compacte une liste d'options.","dropdownCompKeywords":"menu déroulant, menu, options, sélectionner","toggleButtonCompName":"Bouton à bascule","toggleButtonCompDesc":"Un bouton qui peut basculer entre deux états ou options.","toggleButtonCompKeywords":"bascule, bouton, interrupteur, état","textCompName":"Affichage du texte","textCompDesc":"Un composant simple pour afficher un contenu textuel statique ou dynamique incluant la mise en forme Markdown.","textCompKeywords":"texte, affichage, statique, dynamique","tableCompName":"Tableau","tableCompDesc":"Un composant de tableau riche pour afficher des données dans un format de tableau structuré, avec des options de tri et de filtrage, l'affichage de données en arborescence et des rangées extensibles.","tableCompKeywords":"tableau, données, tri, filtrage","imageCompName":"Image","imageCompDesc":"Un composant pour l'affichage d'images, prenant en charge différents formats basés sur des données URI ou Base64.","imageCompKeywords":"image, affichage, média, Base64","progressCompName":"Progrès","progressCompDesc":"Un indicateur visuel de la progression, généralement utilisé pour montrer l'état d'achèvement d'une tâche.","progressCompKeywords":"progrès, indicateur, statut, tâche","progressCircleCompName":"Cercle de progrès","progressCircleCompDesc":"Un indicateur de progrès circulaire, souvent utilisé pour les états de chargement ou les tâches limitées dans le temps.","progressCircleCompKeywords":"cercle, progrès, indicateur, chargement","fileViewerCompName":"Visionneuse de fichiers","fileViewerCompDesc":"Un composant permettant d'afficher divers types de fichiers, notamment des documents et des images.","fileViewerCompKeywords":"fichier, visionneuse, document, image","dividerCompName":"Diviseur","dividerCompDesc":"Un composant de séparation visuelle, utilisé pour séparer le contenu ou les sections dans une mise en page.","dividerCompKeywords":"diviseur, séparateur, mise en page, conception","qrCodeCompName":"Code QR","qrCodeCompDesc":"Un composant permettant d'afficher des codes QR, utiles pour une numérisation rapide et le transfert d'informations.","qrCodeCompKeywords":"QR code, scanner, code-barres, informations","formCompName":"Formulaire","formCompDesc":"Un composant de conteneur pour construire des formulaires structurés avec différents types d'entrée.","formCompKeywords":"formulaire, entrée, conteneur, structure","jsonSchemaFormCompName":"Formulaire de schéma JSON","jsonSchemaFormCompDesc":"Un composant de formulaire dynamique généré sur la base d'un schéma JSON.","jsonSchemaFormCompKeywords":"JSON, schéma, formulaire, dynamique","containerCompName":"Conteneur","containerCompDesc":"Un conteneur à usage général pour la mise en page et l'organisation des éléments de l'interface utilisateur.","containerCompKeywords":"conteneur, mise en page, organisation, interface utilisateur","collapsibleContainerCompName":"Récipient pliable","collapsibleContainerCompDesc":"Un conteneur qui peut être agrandi ou réduit, idéal pour gérer la visibilité du contenu.","collapsibleContainerCompKeywords":"pliable, conteneur, expansion, effondrement","tabbedContainerCompName":"Conteneur à onglets","tabbedContainerCompDesc":"Un conteneur avec navigation par onglets pour organiser le contenu en panneaux distincts.","tabbedContainerCompKeywords":"onglets, conteneur, navigation, panneaux","modalCompName":"Modal","modalCompDesc":"Un composant modal pop-up pour afficher du contenu, des alertes ou des formulaires en focus.","modalCompKeywords":"modal, popup, alerte, formulaire","listViewCompName":"Vue de la liste","listViewCompDesc":"Un composant pour afficher une liste d'éléments ou de données, dans lequel tu peux placer d'autres composants. Comme un répéteur.","listViewCompKeywords":"liste, vue, affichage, répéteur","gridCompName":"Grille","gridCompDesc":"Un composant de grille flexible pour créer des mises en page structurées avec des lignes et des colonnes en tant qu'extension du composant List View.","gridCompKeywords":"grille, disposition, lignes, colonnes","navigationCompName":"Navigation","navigationCompDesc":"Composant de navigation permettant de créer des menus, des fils d'Ariane ou des onglets pour la navigation sur le site.","navigationCompKeywords":"navigation, menu, miettes de pain, onglets","iframeCompName":"IFrame","iframeCompDesc":"Un composant de cadre en ligne pour intégrer des pages web et des apps externes ou du contenu dans l'application.","iframeCompKeywords":"iframe, embed, page web, contenu","customCompName":"Composant personnalisé","customCompDesc":"Un composant flexible et programmable pour créer des éléments d'interface utilisateur uniques, définis par l'utilisateur et adaptés à tes besoins spécifiques.","customCompKeywords":"personnalisé, défini par l'utilisateur, flexible, programmable","moduleCompName":"Module","moduleCompDesc":"Utilise les modules pour créer des micro-applications conçues pour encapsuler des fonctionnalités ou des caractéristiques spécifiques. Les modules peuvent ensuite être intégrés et réutilisés dans toutes les applications.","moduleCompKeywords":"module, micro-app, fonctionnalité, réutilisable","jsonExplorerCompName":"Explorateur JSON","jsonExplorerCompDesc":"Un composant pour explorer visuellement et interagir avec les structures de données JSON.","jsonExplorerCompKeywords":"JSON, explorateur, données, structure","jsonEditorCompName":"Éditeur JSON","jsonEditorCompDesc":"Un composant éditeur pour créer et modifier des données JSON avec validation et coloration syntaxique.","jsonEditorCompKeywords":"JSON, éditeur, modifier, valider","treeCompName":"Arbre","treeCompDesc":"Composant de structure arborescente permettant d'afficher des données hiérarchiques, telles que des systèmes de fichiers ou des organigrammes.","treeCompKeywords":"arbre, hiérarchique, données, structure","treeSelectCompName":"Sélection de l'arbre","treeSelectCompDesc":"Un composant de sélection qui présente les options sous forme d'arbre hiérarchique, permettant des sélections organisées et imbriquées.","treeSelectCompKeywords":"arbre, sélection, hiérarchique, imbriqué","audioCompName":"Audio","audioCompDesc":"Un composant pour intégrer du contenu audio, avec des commandes pour la lecture et le réglage du volume.","audioCompKeywords":"audio, lecture, son, musique","videoCompName":"Vidéo","videoCompDesc":"Un composant multimédia pour l'intégration et la lecture de contenu vidéo, avec prise en charge de divers formats.","videoCompKeywords":"vidéo, multimédia, lecture, intégration","drawerCompName":"Tiroir","drawerCompDesc":"Un composant de panneau coulissant qui peut être utilisé pour une navigation supplémentaire ou l'affichage de contenu, émergeant généralement du bord de l'écran.","drawerCompKeywords":"tiroir, coulissant, panneau, navigation","chartCompName":"Graphique","chartCompDesc":"Un composant polyvalent pour visualiser les données à l'aide de différents types de tableaux et de graphiques.","chartCompKeywords":"diagramme, graphique, données, visualisation","carouselCompName":"Carrousel d'images","carouselCompDesc":"Un composant de carrousel rotatif pour mettre en valeur des images, des bannières ou des diapositives de contenu.","carouselCompKeywords":"carrousel, images, rotation, vitrine","imageEditorCompName":"Éditeur d'images","imageEditorCompDesc":"Un composant interactif pour l'édition et la manipulation d'images, offrant divers outils et filtres.","imageEditorCompKeywords":"image, éditeur, manipuler, outils","mermaidCompName":"Tableau des sirènes","mermaidCompDesc":"Un composant pour rendre les diagrammes complexes et les organigrammes basés sur la syntaxe Mermaid.","mermaidCompKeywords":"sirène, graphiques, diagrammes, organigrammes","calendarCompName":"Calendrier","calendarCompDesc":"Un composant de calendrier pour afficher les dates et les événements, avec des options d'affichage par mois, par semaine ou par jour.","calendarCompKeywords":"calendrier, dates, événements, planification","signatureCompName":"Signature","signatureCompDesc":"Un composant permettant de capturer des signatures numériques, utile pour les processus d'approbation et de vérification.","signatureCompKeywords":"signature, numérique, approbation, vérification","jsonLottieCompName":"Lottie Animation","jsonLottieCompDesc":"Un composant pour afficher les animations Lottie, fournissant des animations légères et évolutives basées sur des données JSON.","jsonLottieCompKeywords":"lottie, animation, JSON, évolutif","timelineCompName":"Chronologie","timelineCompDesc":"Composant permettant d'afficher des événements ou des actions dans un ordre chronologique, représenté visuellement le long d'une ligne de temps linéaire.","timelineCompKeywords":"chronologie, événements, chronologique, histoire","commentCompName":"Commentaire","commentCompDesc":"Un composant permettant d'ajouter et d'afficher des commentaires d'utilisateurs, prenant en charge les réponses par fil de discussion et l'interaction avec l'utilisateur.","commentCompKeywords":"commentaire, discussion, interaction avec l'utilisateur, retour d'information","mentionCompName":"Mention","mentionCompDesc":"Un composant qui prend en charge la mention d'utilisateurs ou de balises dans un contenu textuel, généralement utilisé dans les médias sociaux ou les plateformes collaboratives.","mentionCompKeywords":"mention, tag, utilisateur, médias sociaux","responsiveLayoutCompName":"Mise en page réactive","responsiveLayoutCompDesc":"Composant de mise en page conçu pour s'adapter et répondre aux différentes tailles d'écran et aux différents appareils, ce qui garantit une expérience utilisateur cohérente.","responsiveLayoutCompKeywords":"responsive, layout, adapter, taille d'écran"},"comp":{"menuViewDocs":"Voir la documentation","menuViewPlayground":"Voir l'aire de jeux interactive","menuUpgradeToLatest":"Mise à jour vers la dernière version","nameNotEmpty":"Ne peut pas être vide","nameRegex":"Doit commencer par une lettre et ne contenir que des lettres, des chiffres et des caractères de soulignement (_).","nameJSKeyword":"Ne peut pas être un mot-clé JavaScript","nameGlobalVariable":"Le nom de la variable ne peut pas être global","nameExists":"Le nom {nom} existe déjà","getLatestVersionMetaError":"Le téléchargement de la dernière version a échoué, essaie plus tard.","needNotUpgrade":"La version actuelle est déjà la plus récente.","compNotFoundInLatestVersion":"Composant actuel introuvable dans la dernière version.","upgradeSuccess":"Mise à jour réussie vers la dernière version.","searchProp":"Recherche"},"jsonSchemaForm":{"retry":"Réessayer","resetAfterSubmit":"Réinitialisation après l'envoi du formulaire","jsonSchema":"Schéma JSON","uiSchema":"Schéma de l'interface utilisateur","schemaTooltip":"Voir","defaultData":"Données de formulaire pré-remplies","dataDesc":"Données du formulaire actuel","required":"Exigée","maximum":"La valeur maximale est {valeur}","minimum":"La valeur minimale est {valeur}","exclusiveMaximum":"Doit être inférieur à {valeur}","exclusiveMinimum":"Doit être supérieur à {valeur}","multipleOf":"Doit être un multiple de {valeur}","minLength":"Au moins {valeur} Caractères","maxLength":"Au plus {valeur} Caractères","pattern":"Doit correspondre au modèle {valeur}","format":"Doit correspondre au format {valeur}"},"select":{"inputValueDesc":"Valeur de recherche d'entrée"},"customComp":{"text":"C'est une bonne journée.","triggerQuery":"Requête de déclenchement","updateData":"Mise à jour des données","updateText":"Je suis également de bonne humeur pour développer maintenant mon propre composant personnalisé avec Lowcoder !","sdkGlobalVarName":"Lowcoder","data":"Données que tu veux transmettre au composant personnalisé","code":"Code de ton composant personnalisé"},"tree":{"selectType":"Sélectionne le type","noSelect":"Pas de sélection","singleSelect":"Sélection unique","multiSelect":"Multi Select","checkbox":"Case à cocher","checkedStrategy":"Stratégie vérifiée","showAll":"Tous les nœuds","showParent":"Seulement les nœuds parents","showChild":"Nœuds de l'enfant unique","autoExpandParent":"Auto Expand Parent","checkStrictly":"Vérifier strictement","checkStrictlyTooltip":"Vérifie le nœud de l'arbre avec précision ; le nœud de l'arbre parent et les nœuds de l'arbre enfant ne sont pas associés.","treeData":"Données sur les arbres","treeDataDesc":"Données actuelles sur les arbres","value":"Valeurs par défaut","valueDesc":"Valeurs actuelles","expanded":"Valeurs élargies","expandedDesc":"Valeurs élargies actuelles","defaultExpandAll":"Par défaut Développer tous les nœuds","showLine":"Ligne de spectacle","showLeafIcon":"Montrer l'icône de la feuille","treeDataAsia":"Asie","treeDataChina":"Chine","treeDataBeijing":"Pékin","treeDataShanghai":"Shanghai","treeDataJapan":"Japon","treeDataEurope":"L'Europe","treeDataEngland":"Angleterre","treeDataFrance":"France","treeDataGermany":"Allemagne","treeDataNorthAmerica":"Amérique du Nord","helpLabel":"Étiquette du nœud","helpValue":"Valeur unique du nœud dans l'arbre","helpChildren":"Nœuds des enfants","helpDisabled":"Désactive le nœud","helpSelectable":"Si le nœud est sélectionnable (Type de sélection simple/multi)","helpCheckable":"Afficher ou non la case à cocher (Type de case à cocher)","helpDisableCheckbox":"Désactive la case à cocher (Type de case à cocher)"},"moduleContainer":{"eventTest":"Test de l'événement","methodTest":"Test de la méthode","inputTest":"Test d'entrée"},"password":{"label":"Mot de passe","visibilityToggle":"Afficher la visibilité Bascule"},"richTextEditor":{"toolbar":"Personnaliser la barre d'outils","toolbarDescription":"Tu peux personnaliser la barre d'outils. Pour plus de détails, consulte le site https://quilljs.com/docs/modules/toolbar/.","placeholder":"Saisis tes données, s'il te plaît...","hideToolbar":"Cacher la barre d'outils","content":"Contenu","title":"Titre","save":"Sauvegarde","link":"Lien : ","edit":"Éditer","remove":"Enlever","defaultValue":"Contenu de base"},"numberInput":{"formatter":"Format","precision":"Précision","allowNull":"Autoriser la valeur nulle","thousandsSeparator":"Afficher le séparateur de milliers","controls":"Afficher les boutons d'incrémentation et de décrémentation","step":"Étape","standard":"Standard","percent":"Pourcentage"},"slider":{"step":"Étape","stepTooltip":"La valeur doit être supérieure à 0 et divisible par (Max-Min)"},"rating":{"max":"Valeur nominale max.","allowHalf":"Accorde la moitié des points d'évaluation"},"optionsControl":{"optionList":"Options","option":"Option","optionI":"Option {i}","viewDocs":"Voir les documents","tip":"Les variables \"item\" et \"i\" représentent la valeur et l'index de chaque élément du tableau de données."},"radio":{"options":"Options","horizontal":"Horizontal","horizontalTooltip":"La mise en page horizontale s'enroule sur elle-même lorsqu'elle manque d'espace","vertical":"Vertical","verticalTooltip":"La disposition verticale sera toujours affichée en une seule colonne","autoColumns":"Colonne auto","autoColumnsTooltip":"La disposition en colonnes automatiques réorganise automatiquement l'ordre des articles en fonction de l'espace disponible et s'affiche sous forme de colonnes multiples."},"cascader":{"options":"Données JSON pour afficher les sélections en cascade"},"selectInput":{"valueDesc":"Valeur actuellement sélectionnée","selectedIndexDesc":"L'index de la valeur actuellement sélectionnée, ou -1 si aucune valeur n'est sélectionnée.","selectedLabelDesc":"L'étiquette de la valeur actuellement sélectionnée"},"file":{"typeErrorMsg":"Doit être un nombre avec une unité de taille de fichier valide, ou un nombre d'octets sans unité.","fileEmptyErrorMsg":"Le téléchargement a échoué. La taille du fichier est vide.","fileSizeExceedErrorMsg":"Le téléchargement a échoué. La taille du fichier dépasse la limite.","minSize":"Taille minimale","minSizeTooltip":"La taille minimale des fichiers téléchargés avec des unités de taille de fichier optionnelles (par exemple, \"5kb\", \"10 MB\"). Si aucune unité n'est fournie, la valeur sera considérée comme un nombre d'octets.","maxSize":"Taille maximale","maxSizeTooltip":"La taille maximale des fichiers téléchargés avec des unités de taille de fichier optionnelles (par exemple, \"5kb\", \"10 MB\"). Si aucune unité n'est fournie, la valeur sera considérée comme un nombre d'octets.","single":"Célibataire","multiple":"Multiple","directory":"Répertoire","upload":"Parcourir","fileType":"Types de fichiers","reference":"Réfère-toi à","fileTypeTooltipUrl":"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers","fileTypeTooltip":"Spécification de type de fichier unique","uploadType":"Type de téléchargement","showUploadList":"Afficher la liste des téléchargements","maxFiles":"Fichiers Max","filesValueDesc":"Le contenu du fichier actuellement téléchargé est codé en Base64","filesDesc":"Liste des fichiers actuellement téléchargés. Pour plus de détails, voir","clearValueDesc":"Effacer tous les fichiers","parseFiles":"Analyse les fichiers","parsedValueTooltip1":"Si parseFiles est vrai, les fichiers téléchargés seront analysés en tant qu'objet, tableau ou chaîne. Les données analysées sont accessibles via le tableau parsedValue.","parsedValueTooltip2":"Prend en charge les fichiers Excel, JSON, CSV et texte. Les autres formats renverront un résultat nul."},"date":{"format":"Format","formatTip":"Support : \\N- 'YYYY-MM-DD HH:mm:ss', \\N- 'YYYY-MM-DD', \\N- 'Timestamp' (horodatage)","reference":"Réfère-toi à","showTime":"L'heure du spectacle","start":"Date de début","end":"Date de fin","year":"Année","quarter":"Trimestre","month":"Mois","week":"Semaine","date":"Date","clearAllDesc":"Effacer tout","resetAllDesc":"Réinitialiser tout","placeholder":"Sélectionne la date","placeholderText":"Placeholder","startDate":"Date de début","endDate":"Date de fin"},"time":{"start":"Heure de début","end":"L'heure de la fin","formatTip":"Support : \\HH:mm:ss, Horodatage","format":"Format","placeholder":"Sélectionne l'heure","placeholderText":"Placeholder","startTime":"Heure de début","endTime":"L'heure de la fin"},"button":{"prefixIcon":"Icône de préfixe","suffixIcon":"Icône de suffixe","icon":"Icône","iconSize":"Taille de l'icône","button":"Bouton de formulaire","formToSubmit":"Formulaire à soumettre","default":"Défaut","submit":"Soumettre","textDesc":"Texte actuellement affiché sur le bouton","loadingDesc":"Le bouton est-il en état de chargement ? Si vrai, le bouton actuel est en cours de chargement","formButtonEvent":"Événement"},"link":{"link":"Lien","textDesc":"Texte actuellement affiché sur le lien","loadingDesc":"Le lien est-il en état de chargement ? Si vrai, le lien actuel est en cours de chargement"},"scanner":{"text":"Clique sur Numériser","camera":"Caméra {index}","changeCamera":"Change de caméra","continuous":"Balayage continu","uniqueData":"Ignorer les données en double","maskClosable":"Clique sur le masque pour le fermer","errTip":"Utilise ce composant sous HTTPS ou Localhost"},"dropdown":{"onlyMenu":"Présentoir avec étiquette seulement","textDesc":"Texte actuellement affiché sur le bouton"},"textShow":{"text":"### 👋 Bonjour, {nom}","valueTooltip":"Markdown prend en charge la plupart des balises et attributs HTML. iframe, Script et d'autres balises sont désactivées pour des raisons de sécurité.","verticalAlignment":"Alignement vertical","horizontalAlignment":"Alignement horizontal","textDesc":"Texte affiché dans la zone de texte actuelle"},"table":{"editable":"Modifiable","columnNum":"Colonnes","viewModeResizable":"Largeur de colonne ajustée par l'utilisateur","viewModeResizableTooltip":"Si les utilisateurs peuvent ajuster la largeur des colonnes.","showFilter":"Bouton Afficher le filtre","showRefresh":"Afficher le bouton de rafraîchissement","showDownload":"Afficher le bouton de téléchargement","columnSetting":"Bouton de réglage des colonnes","searchText":"Texte de recherche","searchTextTooltip":"Recherche et filtre les données présentées dans le tableau","showQuickJumper":"Show Quick Jumper","hideOnSinglePage":"Cacher sur une seule page","showSizeChanger":"Bouton de changement de taille","pageSizeOptions":"Options de taille de page","pageSize":"Taille de la page","total":"Nombre total de lignes","totalTooltip":"La valeur par défaut est le nombre d'éléments de données actuels, qui peuvent être obtenus à partir de la requête, par exemple : \\'{{query1.data[0].count}}\\'","filter":"Filtre","filterRule":"Règle de filtrage","chooseColumnName":"Choisis une colonne","chooseCondition":"Choisis une condition","clear":"Clair","columnShows":"Spectacles en colonne","selectAll":"Sélectionner tout","and":"Et","or":"Ou","contains":"Contient","notContain":"Ne contient pas","equals":"Égales","isNotEqual":"N'est pas égal","isEmpty":"Est vide","isNotEmpty":"N'est pas vide","greater":"Plus grand que","greaterThanOrEquals":"Plus grand que ou égal","lessThan":"Moins de","lessThanOrEquals":"Inférieur ou égal","action":"Action","columnValue":"Valeur de la colonne","columnValueTooltip":"\\'{{currentCell}}\\': Données cellulaires actuelles{{currentRow}}\\' : Données de la ligne actuelle{{currentIndex}}\\': Index des données actuelles (à partir de 0)\\NExemple : \\'{{currentCell * 5}}' Afficher 5 fois la valeur d'origine Données.","imageSrc":"Source de l'image","imageSize":"Taille de l'image","columnTitle":"Titre","sortable":"Triable","align":"Alignement","fixedColumn":"Colonne fixe","autoWidth":"Largeur de l'auto","customColumn":"Colonne personnalisée","auto":"Auto","fixed":"Fixe","columnType":"Type de colonne","float":"Flotteur","prefix":"Préfixe","suffix":"Suffixe","text":"Texte","number":"Nombre","link":"Lien","links":"Liens","tag":"Étiquette","date":"Date","dateTime":"Date Heure","badgeStatus":"Statut","button":"Bouton","image":"Image","boolean":"Booléen","rating":"Evaluation","progress":"Progrès","option":"Fonctionnement","optionList":"Liste des opérations","option1":"Opération 1","status":"Statut","statusTooltip":"Valeurs optionnelles : Succès, Erreur, Défaut, Avertissement, Traitement","primaryButton":"Primaire","defaultButton":"Défaut","type":"Type","tableSize":"Taille de la table","hideHeader":"Masquer l'en-tête du tableau","fixedHeader":"En-tête de tableau fixe","fixedHeaderTooltip":"L'en-tête sera fixe pour le tableau à défilement vertical","fixedToolbar":"Barre d'outils fixe","fixedToolbarTooltip":"La barre d'outils sera fixe pour le tableau à défilement vertical en fonction de la position","hideBordered":"Masquer la bordure de la colonne","deleteColumn":"Supprimer une colonne","confirmDeleteColumn":"Confirme la suppression de la colonne : ","small":"S","middle":"M","large":"L","refreshButtonTooltip":"Les données actuelles changent, clique pour régénérer la colonne.","changeSetDesc":"Un objet représentant les modifications apportées à un tableau modifiable ne contient que la cellule modifiée. Les lignes passent en premier et les colonnes en second.","selectedRowDesc":"Fournit des données sur la ligne actuellement sélectionnée, indiquant la ligne qui déclenche un événement de clic si l'utilisateur clique sur un bouton/lien de la ligne.","selectedRowsDesc":"Utile en mode de sélection multiple, comme SelectedRow","pageNoDesc":"Page d'affichage actuelle, à partir de 1","pageSizeDesc":"Combien de lignes par page ?","sortColumnDesc":"Le nom de la colonne triée actuellement sélectionnée","sortDesc":"Si la ligne actuelle est en ordre décroissant","pageOffsetDesc":"Le début actuel de la pagination, utilisé pour la pagination afin d'obtenir des données. Exemple : Select * from Users Limit \\'{{table1.pageSize}}\\N- Décalage \\N- Décalage \\N- Décalage \\N- Décalage{{table1.pageOffset}}\\'","displayDataDesc":"Données affichées dans le tableau actuel","selectedIndexDesc":"Index sélectionné dans les données d'affichage","filterDesc":"Paramètres de filtrage des tables","dataDesc":"Les données JSON du tableau","saveChanges":"Sauvegarder les changements","cancelChanges":"Annuler les changements","rowSelectChange":"Changement de sélection de ligne","rowClick":"Cliquez sur la rangée","rowExpand":"Extension de la rangée","filterChange":"Changement de filtre","sortChange":"Changement de tri","pageChange":"Changement de page","refresh":"Rafraîchir","rowColor":"Couleur de ligne conditionnelle","rowColorDesc":"Définit conditionnellement la couleur de la ligne en fonction des variables facultatives : CurrentRow, CurrentOriginalIndex, CurrentIndex, ColumnTitle. Par exemple : \\'{{ currentRow.id > 3 ? %r@\\\"green%r@\\\" : %r@\\\"red%r@\\\" }}\\'","cellColor":"Couleur conditionnelle des cellules","cellColorDesc":"Définir conditionnellement la couleur de la cellule en fonction de la valeur de la cellule à l'aide de CurrentCell. Par exemple : \\N{ currentCell == 3 ? %r@\\\"green%r@\\\" : %r@\\\"red%r@\\\" }}\\'","saveChangesNotBind":"Aucun gestionnaire d'événements n'est configuré pour enregistrer les modifications. Lier au moins un gestionnaire d'événements avant de cliquer.","dynamicColumn":"Utiliser le réglage dynamique des colonnes","dynamicColumnConfig":"Réglage de la colonne","dynamicColumnConfigDesc":"Paramètres dynamiques des colonnes. Accepte un tableau de noms de colonnes. Toutes les colonnes sont visibles par défaut. Exemple : [%r@\\\"id%r@\\\", %r@\\\"name%r@\\\"]","position":"Position","showDataLoadSpinner":"Montrer le rouleau pendant le chargement des données","showValue":"Montrer la valeur","expandable":"Extensible","configExpandedView":"Configurer la vue étendue","toUpdateRowsDesc":"Un tableau d'objets pour les lignes à mettre à jour dans les tableaux modifiables.","empty":"Vide","falseValues":"Texte si faux","allColumn":"Tous","visibleColumn":"Visible","emptyColumns":"Aucune colonne n'est actuellement visible"},"image":{"src":"Source de l'image","srcDesc":"La source de l'image. Il peut s'agir d'une URL, d'un chemin ou d'une chaîne Base64. par exemple : data:image/png;base64, AAA... CCC","supportPreview":"Support Cliquez sur l'aperçu (zoom)","supportPreviewTip":"Efficace lorsque la source de l'image est valide"},"progress":{"value":"Valeur","valueTooltip":"Le pourcentage d'achèvement est une valeur comprise entre 0 et 100.","showInfo":"Montrer la valeur","valueDesc":"Valeur de la progression actuelle, comprise entre 0 et 100","showInfoDesc":"Afficher ou non la valeur actuelle de la progression"},"fileViewer":{"invalidURL":"Saisis une URL valide ou une chaîne de caractères Base64","src":"URI de fichier","srcTooltip":"Prévisualise le contenu des liens fournis en y intégrant du HTML, les données encodées en Base64 peuvent également être prises en charge, par exemple : data:application/pdf ; base64,AAA... CCC","srcDesc":"L'URI du fichier"},"divider":{"title":"Titre","align":"Alignement","dashed":"En pointillé","dashedDesc":"Utiliser ou non la ligne pointillée","titleDesc":"Titre du diviseur","alignDesc":"Alignement du titre de l'intercalaire"},"QRCode":{"value":"Valeur du contenu du code QR","valueTooltip":"La valeur contient un maximum de 2953 caractères. La Valeur du code QR peut encoder différents types de données, notamment des messages texte, des URL, des coordonnées (VCard/meCard), des identifiants de connexion Wi-Fi, des adresses e-mail, des numéros de téléphone, des SMS, des coordonnées de géolocalisation, des détails d'événements du calendrier, des informations de paiement, des adresses de crypto-monnaies et des liens de téléchargement d'applis","valueDesc":"La valeur du contenu du code QR","level":"Niveau de tolérance aux fautes","levelTooltip":"Se réfère à la capacité du code QR à être scanné même si une partie est bloquée. Plus le niveau est élevé, plus le code est complexe.","includeMargin":"Afficher la marge","image":"Affiche l'image au centre","L":"L (faible)","M":"M (Moyen)","Q":"Q (Quartile)","H":"H (Haut)","maxLength":"Le contenu est trop long. Règle la longueur à moins de 2953 caractères."},"jsonExplorer":{"indent":"Indentation de chaque niveau","expandToggle":"Développer l'arbre JSON","theme":"Thème de couleur","valueDesc":"Données JSON actuelles","default":"Défaut","defaultDark":"Foncé par défaut","neutralLight":"Lumière neutre","neutralDark":"Neutre foncé","azure":"L'azur","darkBlue":"Bleu foncé"},"audio":{"src":"Source audio URI ou chaîne Base64","defaultSrcUrl":"https://cdn.pixabay.com/audio/2023/07/06/audio_e12e5bea9d.mp3","autoPlay":"Jeu automatique","loop":"Boucle","srcDesc":"URI audio actuel ou chaîne Base64 comme data:audio/mpeg;base64,AAA... CCC","play":"Jouer","playDesc":"Déclenché lors de la lecture d'un fichier audio","pause":"Pause","pauseDesc":"Déclenché lorsque l'audio est en pause","ended":"Terminé","endedDesc":"Déclenché à la fin de la lecture de l'audio"},"video":{"src":"URI de la source vidéo ou chaîne Base64","defaultSrcUrl":"https://www.youtube.com/watch?v=pRpeEdMmmQ0","poster":"URL de l'affiche","defaultPosterUrl":"","autoPlay":"Jeu automatique","loop":"Boucle","controls":"Cacher les contrôles","volume":"Volume","playbackRate":"Taux de lecture","posterTooltip":"La valeur par défaut est la première image de la vidéo.","autoPlayTooltip":"Une fois que la vidéo est chargée, elle est lue automatiquement. Si tu changes cette valeur de True à False, la vidéo sera mise en pause. (Si un poster est défini, il sera lu par le bouton Poster)","controlsTooltip":"Cache les contrôles de lecture vidéo. Peut ne pas être entièrement pris en charge par toutes les sources vidéo.","volumeTooltip":"Règle le volume du lecteur, entre 0 et 1","playbackRateTooltip":"Règle le taux du joueur, entre 1 et 2","srcDesc":"URI audio actuel ou chaîne Base64 comme data:video/mp4;base64, AAA... CCC","play":"Jouer","playDesc":"Déclenché lors de la lecture de la vidéo","pause":"Pause","pauseDesc":"Déclenché lorsque la vidéo est mise en pause","load":"Charge","loadDesc":"Déclenché lorsque le chargement de la ressource vidéo est terminé","ended":"Terminé","endedDesc":"Déclenché à la fin de la lecture de la vidéo","currentTimeStamp":"La position de lecture actuelle de la vidéo en secondes","duration":"Durée totale de la vidéo en secondes"},"media":{"playDesc":"Commence la lecture du média.","pauseDesc":"Met en pause la lecture du média.","loadDesc":"Réinitialise le média au début et redémarre Sélectionne la ressource média.","seekTo":"Cherche jusqu'au nombre de secondes donné, ou une fraction si la quantité est comprise entre 0 et 1","seekToAmount":"Nombre de secondes, ou fraction s'il est compris entre 0 et 1","showPreview":"Avant-première du spectacle"},"rangeSlider":{"start":"Valeur de départ","end":"Valeur finale","step":"Taille de l'étape","stepTooltip":"Granularité du curseur, la valeur doit être supérieure à 0 et divisible par (Max-Min)."},"iconControl":{"selectIcon":"Sélectionne une icône","insertIcon":"Insérer une icône","insertImage":"Insérer une image ou "},"millisecondsControl":{"timeoutTypeError":"Saisis la période de temporisation correcte en ms, l'entrée actuelle est : {valeur}","timeoutLessThanMinError":"L'entrée doit être supérieure à {left}, l'entrée actuelle est : {valeur}"},"selectionControl":{"single":"Célibataire","multiple":"Multiple","close":"Fermer","mode":"Sélectionne le mode"},"container":{"title":"Titre du conteneur affiché"},"drawer":{"placement":"Placement des tiroirs","size":"Taille","top":"Haut","right":"Droit","bottom":"Le fond","left":"Gauche","widthTooltip":"Pixel ou pourcentage, par exemple 520, 60%","heightTooltip":"Pixel, par exemple 378","openDrawerDesc":"Tiroir ouvert","closeDrawerDesc":"Fermer le tiroir","width":"Largeur du tiroir","height":"Hauteur du tiroir"},"meeting":{"logLevel":"Niveau du journal du SDK Agora","placement":"Placement des tiroirs de réunion","meeting":"Paramètres de la réunion","cameraView":"Vue de la caméra","cameraViewDesc":"Vue de la caméra de l'utilisateur local (hôte)","screenShared":"Écran partagé","screenSharedDesc":"Écran partagé par l'utilisateur local (hôte)","audioUnmuted":"Audio Unmuted","audioMuted":"Audio Muted","videoClicked":"Vidéo cliquée","videoOff":"Video Off","videoOn":"Vidéo sur","size":"Taille","top":"Haut","host":"Hôte de la salle de réunion. Tu dois gérer l'hôte comme une application logique propre.","participants":"Participants de la salle de réunion","shareScreen":"Écran d'affichage partagé par l'utilisateur local","appid":"ID de l'application Agora","meetingName":"Nom de la réunion","localUserID":"ID de l'utilisateur de l'hôte","userName":"Nom d'utilisateur de l'hôte","rtmToken":"Token RTM Agora","rtcToken":"Jeton RTC Agora","noVideo":"Pas de vidéo","profileImageUrl":"URL de l'image du profil","right":"Droit","bottom":"Le fond","videoId":"ID du flux vidéo","audioStatus":"État de l'audio","left":"Gauche","widthTooltip":"Pixel ou pourcentage, par exemple 520, 60%","heightTooltip":"Pixel, par exemple 378","openDrawerDesc":"Tiroir ouvert","closeDrawerDesc":"Fermer le tiroir","width":"Largeur du tiroir","height":"Hauteur du tiroir","actionBtnDesc":"Bouton d'action","broadCast":"Messages diffusés","title":"Titre de la réunion","meetingCompName":"Agora Meeting Controller","sharingCompName":"Partage d'écran Flux","videoCompName":"Flux de caméra","videoSharingCompName":"Partage d'écran Flux","meetingControlCompName":"Bouton de commande","meetingCompDesc":"Composante de la réunion","meetingCompControls":"Contrôle des réunions","meetingCompKeywords":"Agora Meeting, Web Meeting, Collaboration","iconSize":"Taille de l'icône","userId":"ID de l'utilisateur de l'hôte","roomId":"ID de la pièce","meetingActive":"Réunion en cours","messages":"Messages diffusés"},"settings":{"title":"Réglages","userGroups":"Groupes d'utilisateurs","organization":"Espaces de travail","audit":"Journaux d'audit","theme":"Thèmes","plugin":"Plugins","advanced":"Avancé","lab":"Laboratoire","branding":"L'image de marque","oauthProviders":"Fournisseurs OAuth","appUsage":"Journal d'utilisation de l'application","environments":"Environnements","premium":"Premium"},"memberSettings":{"admin":"Admin","adminGroupRoleInfo":"L'administrateur peut gérer les membres et les ressources du groupe","adminOrgRoleInfo":"Les administrateurs possèdent toutes les ressources et peuvent gérer les groupes.","member":"Membre","memberGroupRoleInfo":"Le membre peut voir les membres du groupe","memberOrgRoleInfo":"Les membres ne peuvent utiliser ou visiter que les ressources auxquelles ils ont accès.","title":"Les membres","createGroup":"Créer un groupe","newGroupPrefix":"Nouveau groupe ","allMembers":"Tous les membres","deleteModalTitle":"Supprimer ce groupe","deleteModalContent":"Le groupe supprimé ne peut pas être restauré. Es-tu sûr de vouloir supprimer le groupe ?","addMember":"Ajouter des membres","nameColumn":"Nom de l'utilisateur","joinTimeColumn":"Temps d'adhésion","actionColumn":"Fonctionnement","roleColumn":"Rôle","exitGroup":"Groupe de sortie","moveOutGroup":"Retirer du groupe","inviteUser":"Invite les membres","exitOrg":"Pars","exitOrgDesc":"Es-tu sûr de vouloir quitter cet espace de travail.","moveOutOrg":"Enlever","moveOutOrgDescSaasMode":"Es-tu sûr de vouloir supprimer l'utilisateur {nom} de cet espace de travail ?","moveOutOrgDesc":"Es-tu sûr de vouloir supprimer l'utilisateur {nom} ? Cette action ne peut pas être récupérée.","devGroupTip":"Les membres du groupe de développeurs ont des privilèges pour créer des applications et des sources de données.","lastAdminQuit":"Le dernier administrateur ne peut pas quitter.","organizationNotExist":"L'espace de travail actuel n'existe pas","inviteUserHelp":"Tu peux copier le lien d'invitation pour l'envoyer à l'utilisateur","inviteUserLabel":"Lien d'invitation :","inviteCopyLink":"Copier le lien","inviteText":"{nom d'utilisateur} t'invite à rejoindre l'espace de travail %r@\\\"{organisation}%r@\\\", clique sur le lien pour le rejoindre : {inviteLink}","groupName":"Nom du groupe","createTime":"Créer du temps","manageBtn":"Gérer","userDetail":"Détail","syncDeleteTip":"Ce groupe a été supprimé du carnet d'adresses Source","syncGroupTip":"Ce groupe est un groupe de synchronisation du carnet d'adresses et ne peut pas être modifié."},"orgSettings":{"newOrg":"Nouvel espace de travail (Organisation)","title":"Espace de travail","createOrg":"Créer un espace de travail (organisation)","deleteModalTitle":"Es-tu sûr de vouloir supprimer cet espace de travail ?","deleteModalContent":"Tu es sur le point de supprimer cet espace de travail {permanentlyDelete}. Une fois supprimé, l'espace de travail {notRestored}.","permanentlyDelete":"De façon permanente","notRestored":"Ne peut pas être restauré","deleteModalLabel":"Saisis le nom de l'espace de travail {nom} pour confirmer l'opération :","deleteModalTip":"Saisis le nom de l'espace de travail","deleteModalErr":"Le nom de l'espace de travail est incorrect","deleteModalBtn":"Supprimer","editOrgTitle":"Modifier les informations sur l'espace de travail","orgNameLabel":"Nom de l'espace de travail :","orgNameCheckMsg":"Le nom de l'espace de travail ne peut pas être vide","orgLogo":"Logo de l'espace de travail :","logoModify":"Modifier l'image","inviteSuccessMessage":"Réussir à rejoindre l'espace de travail","inviteFailMessage":"Échec de la jonction avec l'espace de travail","uploadErrorMessage":"Erreur de téléchargement","orgName":"Nom de l'espace de travail"},"freeLimit":"Essai gratuit","tabbedContainer":{"switchTab":"Onglet \"Switch\" (interrupteur)","switchTabDesc":"Déclenché lors du passage d'un onglet à l'autre","tab":"Onglets","atLeastOneTabError":"Le conteneur d'onglets conserve au moins un onglet.","selectedTabKeyDesc":"Onglet actuellement sélectionné","iconPosition":"Position de l'icône"},"formComp":{"containerPlaceholder":"Fais glisser les composants depuis le panneau de droite ou","openDialogButton":"Génère un formulaire à partir d'une de tes sources de données","resetAfterSubmit":"Réinitialisation après une soumission réussie","initialData":"Données initiales","disableSubmit":"Désactiver la soumission","success":"Formulaire généré avec succès","selectCompType":"Sélectionne le type de composant","dataSource":"Source des données : ","selectSource":"Sélectionne la source","table":"Tableau : ","selectTable":"Sélectionne une table","columnName":"Nom de la colonne","dataType":"Type de données","compType":"Type de composant","required":"Exigée","generateForm":"Générer un formulaire","compSelectionError":"Type de colonne non configuré","compTypeNameError":"Impossible d'obtenir le nom du type de composant","noDataSourceSelected":"Aucune source de données sélectionnée","noTableSelected":"Aucune table sélectionnée","noColumn":"Pas de colonne","noColumnSelected":"Aucune colonne sélectionnée","noDataSourceFound":"Aucune source de données prise en charge n'a été trouvée. Créer une nouvelle source de données","noTableFound":"Aucun tableau n'a été trouvé dans cette source de données, choisis une autre source de données.","noColumnFound":"Aucune colonne prise en charge n'a été trouvée dans ce tableau. Choisis un autre tableau","formTitle":"Titre du formulaire","name":"Nom","nameTooltip":"Le nom de l'attribut dans les données du formulaire, lorsqu'il est laissé en blanc, prend par défaut le nom du composant.","notSupportMethod":"Non pris en charge Méthodes : ","notValidForm":"Le formulaire n'est pas valide","resetDesc":"Réinitialiser les données du formulaire à la valeur par défaut","clearDesc":"Effacer les données du formulaire","setDataDesc":"Définir les données du formulaire","valuesLengthError":"Paramètre Numéro Erreur","valueTypeError":"Type de paramètre Erreur","dataDesc":"Données du formulaire actuel","loadingDesc":"Le formulaire est-il en train de se charger ?"},"modalComp":{"close":"Fermer","closeDesc":"Déclenché lorsque la boîte de dialogue modale est fermée","openModalDesc":"Ouvre la boîte de dialogue","closeModalDesc":"Ferme la boîte de dialogue","visibleDesc":"Est-il visible ? Si c'est le cas, la boîte de dialogue actuelle s'affichera.","modalHeight":"Hauteur modale","modalHeightTooltip":"Pixel, Exemple : 222","modalWidth":"Largeur modale","modalWidthTooltip":"Nombre ou pourcentage, exemple : 520, 60%"},"listView":{"noOfRows":"Nombre de rangs","noOfRowsTooltip":"Nombre de lignes dans la liste - Généralement défini par une variable (par exemple, \"\\\"){{query1.data.length}}\\') pour présenter les résultats de la requête","noOfColumns":"Nombre de colonnes","itemIndexName":"Nom de l'index de l'élément de données","itemIndexNameDesc":"Nom de la variable se référant à l'index de l'article, par défaut {default}","itemDataName":"Nom de l'objet de l'élément de données","itemDataNameDesc":"Nom de la variable faisant référence à l'objet de données de l'élément, par défaut {default}","itemsDesc":"Exposer les données des composants dans une liste","dataDesc":"Les données JSON utilisées dans la liste actuelle","dataTooltip":"Si tu ne définis qu'un nombre, ce champ sera considéré comme le nombre de lignes et les données seront considérées comme vides."},"navigation":{"addText":"Ajouter un élément de sous-menu","logoURL":"Navigation Logo URL","horizontalAlignment":"Alignement horizontal","logoURLDesc":"Tu peux afficher un logo sur le côté gauche en entrant une valeur URI ou une chaîne Base64 comme data:image/png;base64,AAA... CCC","itemsDesc":"Éléments du menu de navigation hiérarchique"},"droppadbleMenuItem":{"subMenu":"Sous-menu {numéro}"},"navItemComp":{"active":"Actif"},"iframe":{"URLDesc":"L'URL source du contenu de l'IFrame. Assure-toi que l'URL est HTTPS ou localhost. Assure-toi également que l'URL n'est pas bloquée par la politique de sécurité du contenu (CSP) du navigateur. L'en-tête \"X-Frame-Options\" ne doit pas avoir pour valeur \"DENY\" ou \"SAMEORIGIN\".","allowDownload":"Autoriser les téléchargements","allowSubmitForm":"Autoriser le formulaire de soumission","allowMicrophone":"Autoriser le microphone","allowCamera":"Autoriser l'appareil photo","allowPopup":"Autoriser les fenêtres contextuelles"},"switchComp":{"defaultValue":"Valeur booléenne par défaut","open":"Sur","close":"Off","openDesc":"Déclenché lorsque l'interrupteur est mis en marche","closeDesc":"Déclenché lorsque l'interrupteur est éteint","valueDesc":"État actuel du commutateur"},"signature":{"tips":"Texte de l'indice","signHere":"Signe ici","showUndo":"Afficher l'annulation","showClear":"Afficher l'effacement"},"localStorageComp":{"valueDesc":"Tous les éléments de données actuellement stockés","setItemDesc":"Ajouter un article","removeItemDesc":"Retirer un article","clearItemDesc":"Effacer tous les articles"},"utilsComp":{"openUrl":"Ouvrir l'URL","openApp":"Ouvrir l'application","copyToClipboard":"Copier dans le presse-papiers","downloadFile":"Télécharger le fichier"},"messageComp":{"info":"Envoyer une notification","success":"Envoyer une notification de réussite","warn":"Envoyer une notification d'avertissement","error":"Envoyer une notification d'erreur"},"themeComp":{"switchTo":"Thème de l'interrupteur"},"transformer":{"preview":"Aperçu","docLink":"En savoir plus sur Transformers...","previewSuccess":"Aperçu du succès","previewFail":"Échec de la prévisualisation","deleteMessage":"Effacer le succès du transformateur. Tu peux utiliser {undoKey} pour annuler.","documentationText":"Les transformateurs sont conçus pour la transformation des données et la réutilisation de ton code JavaScript à plusieurs lignes. Utilise les transformateurs pour adapter les données des requêtes ou des composants aux besoins de ton application locale. Contrairement aux requêtes JavaScript, les transformateurs sont conçus pour effectuer des opérations en lecture seule, ce qui signifie que tu ne peux pas déclencher une requête ou mettre à jour un état temporaire à l'intérieur d'un transformateur."},"temporaryState":{"value":"Valeur d'initialisation","valueTooltip":"La valeur initiale stockée dans l'état temporaire peut être n'importe quelle valeur JSON valide.","docLink":"En savoir plus sur les États temporaires...","pathTypeError":"Le chemin doit être soit une chaîne, soit un tableau de valeurs","unStructuredError":"Les données non structurées {prev} ne peuvent pas être mises à jour par {chemin}","valueDesc":"Valeur de l'état temporaire","deleteMessage":"L'état temporaire est supprimé avec succès. Tu peux utiliser {undoKey} pour annuler.","documentationText":"Les états temporaires dans Lowcoder sont une fonctionnalité puissante utilisée pour gérer des variables complexes qui mettent à jour dynamiquement l'état des composants de ton application. Ces états servent de stockage intermédiaire ou transitoire pour les données qui peuvent changer au fil du temps en raison des interactions de l'utilisateur ou d'autres processus."},"dataResponder":{"data":"Données","dataDesc":"Données du répondant actuel","dataTooltip":"Lorsque ces données sont modifiées, elles déclenchent des actions ultérieures.","docLink":"En savoir plus sur les Data Responders...","deleteMessage":"Le répondeur de données est supprimé avec succès. Tu peux utiliser {undoKey} pour annuler.","documentationText":"Lorsque tu développes une application, tu peux assigner des événements aux composants pour surveiller les modifications apportées à des données spécifiques. Par exemple, un composant Table peut avoir des événements tels que %r@\\\"Row select change%r@\\\", %r@\\\"Filter change%r@\\\", %r@\\\"Sort change%r@\\\", et %r@\\\"Page change%r@\\\" pour suivre les modifications de la propriété selectedRow. Cependant, pour les changements dans les états temporaires, les transformateurs ou les résultats des requêtes, lorsque les événements standard ne sont pas disponibles, les répondeurs de données sont utilisés. Ils te permettent de détecter et de réagir à toute modification des données."},"theme":{"title":"Thèmes","createTheme":"Créer un thème","themeName":"Nom du thème :","themeNamePlaceholder":"Saisis un nom de thème","defaultThemeTip":"Thème par défaut :","createdThemeTip":"Le thème que tu as créé :","option":"Option{index}","input":"Entrée","confirm":"Ok","emptyTheme":"Aucun thème disponible","click":"","toCreate":"","nameColumn":"Nom","defaultTip":"Défaut","updateTimeColumn":"Heure de mise à jour","edit":"Éditer","cancelDefaultTheme":"Thème par défaut non défini","setDefaultTheme":"Définir comme thème par défaut","copyTheme":"Thème dupliqué","setSuccessMsg":"Réglage réussi","cancelSuccessMsg":"Unsetting Succeeded","deleteSuccessMsg":"Suppression réussie","checkDuplicateNames":"Le nom du thème existe déjà, saisis-le à nouveau.","copySuffix":" Copie","saveSuccessMsg":"Sauvegardé avec succès","leaveTipTitle":"Conseils","leaveTipContent":"Tu n'es pas encore sauvée, confirme ton départ ?","leaveTipOkText":"Pars","goList":"Retour à la liste","saveBtn":"Sauvegarde","mainColor":"Couleurs principales","text":"Couleurs du texte","defaultTheme":"Défaut","yellow":"Jaune","green":"Vert","previewTitle":"Aperçu du thème Exemple de composants qui utilisent les couleurs de ton thème","dateColumn":"Date","emailColumn":"Courriel","phoneColumn":"Téléphone","subTitle":"Titre","linkLabel":"Lien","linkUrl":"app.lowcoder.cloud","progressLabel":"Progrès","sliderLabel":"Coulisses","radioLabel":"Radio","checkboxLabel":"Case à cocher","buttonLabel":"Bouton de formulaire","switch":"Interrupteur","previewDate":"16/10/2022","previewEmail1":"ted.com","previewEmail2":"skype.com","previewEmail3":"imgur.com","previewEmail4":"globo.com","previewPhone1":"+63-317-333-0093","previewPhone2":"+30-668-580-6521","previewPhone3":"+86-369-925-2071","previewPhone4":"+7-883-227-8093","chartPreviewTitle":"Aperçu du style de graphique","chartSpending":"Dépenses","chartBudget":"Budget","chartAdmin":"Administration","chartFinance":"Finances","chartSales":"Vente","chartFunnel":"Diagramme en entonnoir","chartShow":"Montrer","chartClick":"Clique sur","chartVisit":"Visiter","chartQuery":"Demande","chartBuy":"Acheter"},"pluginSetting":{"title":"Plugins","npmPluginTitle":"npm Plugins","npmPluginDesc":"Configure les plugins npm pour toutes les applications de l'espace de travail actuel.","npmPluginEmpty":"Aucun plugin npm n'a été ajouté.","npmPluginAddButton":"Ajouter un plugin npm","saveSuccess":"Sauvegardé avec succès"},"advanced":{"title":"Avancé","defaultHomeTitle":"Page d'accueil par défaut","defaultHomeHelp":"La page d'accueil est l'application que tous les non-développeurs verront par défaut lorsqu'ils se connecteront. Note : Assure-toi que l'application sélectionnée est accessible aux non-développeurs.","defaultHomePlaceholder":"Sélectionne la page d'accueil par défaut","saveBtn":"Sauvegarde","preloadJSTitle":"Précharger JavaScript","preloadJSHelp":"Configure le code JavaScript préchargé pour toutes les applications de l'espace de travail actuel.","preloadCSSTitle":"Précharger le CSS","preloadCSSHelp":"Configure le code CSS préchargé pour toutes les applications de l'espace de travail actuel.","preloadCSSApply":"Appliquer à la page d'accueil de l'espace de travail","preloadLibsTitle":"Bibliothèque JavaScript","preloadLibsHelp":"Les bibliothèques JavaScript sont préchargées pour toutes les applications de l'espace de travail actuel, et le système intègre lodash, day.js, uuid, numbro pour une utilisation directe. Les bibliothèques JavaScript sont chargées avant l'initialisation de l'application, ce qui a un certain impact sur les performances de l'application.","preloadLibsEmpty":"Aucune bibliothèque JavaScript n'a été ajoutée","preloadLibsAddBtn":"Ajouter une bibliothèque","saveSuccess":"Sauvegardé avec succès","AuthOrgTitle":"Écran de bienvenue de l'espace de travail","AuthOrgDescrition":"L'URL permettant à tes utilisateurs de se connecter à l'espace de travail actuel."},"branding":{"title":"L'image de marque","logoTitle":"Logo","logoHelp":".JPG, .SVG ou .PNG uniquement","faviconTitle":"Favicon","faviconHelp":".JPG, .SVG ou .PNG uniquement","brandNameTitle":"Nom de la marque","headColorTitle":"Couleur de la tête","save":"Sauvegarde","saveSuccessMsg":"Sauvegardé avec succès","upload":"Cliquer pour télécharger"},"networkMessage":{"0":"Échec de la connexion au serveur, vérifie ton réseau","401":"Échec de l'authentification, reconnecte-toi","403":"Pas de permission, contacte l'administrateur pour obtenir une autorisation.","500":"Service occupé, merci de réessayer plus tard","timeout":"Délai de requête"},"share":{"title":"Partager","viewer":"Visionneuse","editor":"Éditeur","owner":"Propriétaire","datasourceViewer":"Peut utiliser","datasourceOwner":"Peut gérer"},"debug":{"title":"Titre","switch":"Composant du commutateur : "},"module":{"emptyText":"Pas de données","circularReference":"Référence circulaire, le module/l'application en cours ne peut pas être utilisé(e) !","emptyTestInput":"Le module actuel n'a pas d'entrée à tester","emptyTestMethod":"Le module actuel n'a pas de méthode pour tester","name":"Nom","input":"Entrée","params":"Params","emptyParams":"Aucun paramètre n'a été ajouté","emptyInput":"Aucune entrée n'a été ajoutée","emptyMethod":"Aucune méthode n'a été ajoutée","emptyOutput":"Aucune sortie n'a été ajoutée","data":"Données","string":"Chaîne","number":"Nombre","array":"Array","boolean":"Booléen","query":"Demande","autoScaleCompHeight":"Balances à hauteur de composant avec conteneur","excuteMethod":"Exécuter la méthode {nom}","method":"Méthode","action":"Action","output":"Sortie","nameExists":"Nom {nom} existe déjà","eventTriggered":"L'événement {nom} est déclenché","globalPromptWhenEventTriggered":"Affiche une invite globale lorsqu'un événement est déclenché","emptyEventTest":"Le module actuel n'a aucun événement à tester","emptyEvent":"Aucun événement n'a été ajouté","event":"Événement"},"resultPanel":{"returnFunction":"La valeur de retour est une fonction.","consume":"{heure}","JSON":"Montrer JSON"},"createAppButton":{"creating":"Créer...","created":"Créer {nom}"},"apiMessage":{"authenticationFail":"L'authentification de l'utilisateur a échoué, tu dois te reconnecter.","verifyAccount":"Besoin de vérifier le compte","functionNotSupported":"La version actuelle ne prend pas en charge cette fonction. Contacte l'équipe commerciale de Lowcoder pour mettre à jour ton compte."},"globalErrorMessage":{"createCompFail":"Créer le composant {comp} Échec","notHandledError":"{méthode} Méthode non exécutée"},"aggregation":{"navLayout":"Barre de navigation","chooseApp":"Choisis l'application","iconTooltip":"Prend en charge le lien src de l'image ou la chaîne Base64 comme data:image/png;base64,AAA... CCC","hideWhenNoPermission":"Caché pour les utilisateurs non autorisés","queryParam":"Paramètres de la requête URL","hashParam":"URL Hash Params","tabBar":"Barre d'onglets","emptyTabTooltip":"Configure cette page dans le volet de droite"},"appSetting":{"450":"450px (Téléphone)","800":"800px (Tablette)","1440":"1440px (ordinateur portable)","1920":"1920px (écran large)","3200":"3200px (Super grand écran)","title":"Paramètres généraux de l'application","autofill":"Remplissage automatique","userDefined":"Sur mesure","default":"Défaut","tooltip":"Ferme la fenêtre contextuelle après le réglage","canvasMaxWidth":"Largeur maximale du canevas pour cette application","userDefinedMaxWidth":"Largeur maximale personnalisée","inputUserDefinedPxValue":"Saisis une valeur de pixel personnalisée","maxWidthTip":"La largeur maximale doit être supérieure ou égale à 350","themeSetting":"Thème de style appliqué","themeSettingDefault":"Défaut","themeCreate":"Créer un thème"},"customShortcut":{"title":"Raccourcis personnalisés","shortcut":"Raccourci","action":"Action","empty":"Pas de raccourcis","placeholder":"Appuyer sur Raccourci","otherPlatform":"Autre","space":"L'espace"},"profile":{"orgSettings":"Paramètres de l'espace de travail","switchOrg":"Change d'espace de travail","joinedOrg":"Mes espaces de travail","createOrg":"Créer un espace de travail","logout":"Déconnecte-toi","personalInfo":"Mon profil","bindingSuccess":"Liaison {nom de la source} Succès","uploadError":"Erreur de téléchargement","editProfilePicture":"Modifier","nameCheck":"Le nom ne peut pas être vide","name":"Nom : ","namePlaceholder":"Saisis ton nom","toBind":"Pour relier","binding":"Est contraignant","bindError":"Erreur de paramètre, actuellement non pris en charge Liaison.","bindName":"Lier {nom}","loginAfterBind":"Après la liaison, tu peux utiliser {nom} pour te connecter","bindEmail":"Lier l'email :","email":"Courriel","emailCheck":"Saisis un courriel valide","emailPlaceholder":"Saisis ton courriel","submit":"Soumettre","bindEmailSuccess":"Succès de la reliure par courriel","passwordModifiedSuccess":"Le mot de passe a été modifié avec succès","passwordSetSuccess":"Le mot de passe a été défini avec succès","oldPassword":"Ancien mot de passe :","inputCurrentPassword":"Saisis ton mot de passe actuel","newPassword":"Nouveau mot de passe :","inputNewPassword":"Saisis ton nouveau mot de passe","confirmNewPassword":"Confirme le nouveau mot de passe :","inputNewPasswordAgain":"Saisis à nouveau ton nouveau mot de passe","password":"Mot de passe :","modifyPassword":"Modifier le mot de passe","setPassword":"Définir le mot de passe","alreadySetPassword":"Mot de passe défini","setPassPlaceholder":"Tu peux te connecter avec ton mot de passe","setPassAfterBind":"Tu peux définir un mot de passe après la liaison du compte","socialConnections":"Connexions sociales"},"shortcut":{"shortcutList":"Raccourcis clavier","click":"Clique sur","global":"Global","toggleShortcutList":"Afficher les raccourcis clavier","editor":"Éditeur","toggleLeftPanel":"Basculer le volet gauche","toggleBottomPanel":"Basculer le volet inférieur","toggleRightPanel":"Basculer le volet droit","toggleAllPanels":"Basculer tous les panneaux","preview":"Aperçu","undo":"Annuler","redo":"Refaire","showGrid":"Afficher la grille","component":"Composant","multiSelect":"Sélectionne plusieurs","selectAll":"Sélectionner tout","copy":"Copie","cut":"Couper","paste":"Coller","move":"Déplacer","zoom":"Redimensionner","delete":"Supprimer","deSelect":"Désélectionne","queryEditor":"Éditeur de requêtes","excuteQuery":"Exécuter la requête actuelle","editBox":"Éditeur de texte","formatting":"Format","openInLeftPanel":"Ouvrir dans le volet gauche"},"help":{"videoText":"Vue d'ensemble","onBtnText":"OK","permissionDenyTitle":"💡 Impossible de créer une nouvelle application ou une nouvelle source de données ?","permissionDenyContent":"Tu n'as pas la permission de créer l'application et la source de données. Contacte l'administrateur pour rejoindre le groupe de développeurs.","appName":"Tutoriel d'application","chat":"Chat avec nous","docs":"Voir la documentation","editorTutorial":"Tutoriel de l'éditeur","update":"Quoi de neuf ?","version":"Version","versionWithColon":"Version : ","submitIssue":"Soumettre une question"},"header":{"nameCheckMessage":"Le nom ne peut pas être vide","viewOnly":"Voir seulement","recoverAppSnapshotTitle":"Restaurer cette version ?","recoverAppSnapshotContent":"Restaurer l'application actuelle à la version créée à {heure}.","recoverAppSnapshotMessage":"Restaurer cette version","returnEdit":"Retour à l'éditeur","deploy":"Publie","export":"Exporter vers JSON","editName":"Modifier le nom","duplicate":"Duplicata {type}","snapshot":"Histoire","scriptsAndStyles":"Scripts et style","appSettings":"Paramètres de l'application","preview":"Aperçu","editError":"Mode de prévisualisation de l'historique, aucune opération n'est prise en charge.","clone":"Clone","editorMode_layout":"Mise en page","editorMode_logic":"Logique","editorMode_both":"Les deux"},"userAuth":{"registerByEmail":"S'inscrire","email":"Courriel :","inputEmail":"Saisis ton courriel","inputValidEmail":"Saisis un courriel valide","register":"S'inscrire","userLogin":"S'inscrire","login":"S'inscrire","bind":"Relier","passwordCheckLength":"Au moins {min} Personnages","passwordCheckContainsNumberAndLetter":"Doit contenir des lettres et des chiffres","passwordCheckSpace":"Ne peut pas contenir de caractères d'espacement","welcomeTitle":"Bienvenue sur le site de {nom du produit}","inviteWelcomeTitle":"{nom d'utilisateur} T'invite à te connecter {nom du produit}","terms":"Conditions","privacy":"Politique de confidentialité","registerHint":"J'ai lu et j'accepte les","chooseAccount":"Choisis ton compte","signInLabel":"Se connecter avec {nom}","bindAccount":"Relier le compte","scanQrCode":"Scanne le code QR avec {nom}","invalidThirdPartyParam":"Param tiers invalide","account":"Compte","inputAccount":"Saisis ton compte","ldapLogin":"Connexion LDAP","resetPassword":"Réinitialiser le mot de passe","resetPasswordDesc":"Réinitialiser le mot de passe de l'utilisateur {nom}. Un nouveau mot de passe sera généré après la réinitialisation.","resetSuccess":"Réinitialisation réussie","resetSuccessDesc":"La réinitialisation du mot de passe a réussi. Le nouveau mot de passe est : {mot de passe}","copyPassword":"Copier le mot de passe","poweredByLowcoder":"Propulsé par Lowcoder.cloud"},"preLoad":{"jsLibraryHelpText":"Ajoute des bibliothèques JavaScript à ton application actuelle via des adresses URL. lodash, day.js, uuid, numbro sont intégrés au système pour une utilisation immédiate. Les bibliothèques JavaScript sont chargées avant l'initialisation de l'application, ce qui peut avoir un impact sur les performances de l'application.","exportedAs":"Exporté en tant que","urlTooltip":"Adresse URL de la bibliothèque JavaScript, [unpkg.com](https://unpkg.com/) ou [jsdelivr.net](https://www.jsdelivr.com/) est recommandée.","recommended":"Recommandé","viewJSLibraryDocument":"Document","jsLibraryURLError":"URL invalide","jsLibraryExist":"La bibliothèque JavaScript existe déjà","jsLibraryEmptyContent":"Aucune bibliothèque JavaScript n'a été ajoutée","jsLibraryDownloadError":"Erreur de téléchargement de la bibliothèque JavaScript","jsLibraryInstallSuccess":"La bibliothèque JavaScript a été installée avec succès","jsLibraryInstallFailed":"L'installation de la bibliothèque JavaScript a échoué","jsLibraryInstallFailedCloud":"La bibliothèque n'est peut-être pas disponible dans le bac à sable, [Documentation](https://docs.lowcoder.cloud/build-apps/write-javascript/use-third-party-libraries#manually-import-libraries)\\n{message}","jsLibraryInstallFailedHost":"{message}","add":"Ajouter un nouveau","jsHelpText":"Ajoute une méthode ou une variable globale à l'application en cours.","cssHelpText":"Ajouter des styles à l'application en cours. La structure du DOM peut changer au fur et à mesure de l'itération du système. Essayer de modifier les styles par le biais des propriétés des composants.","scriptsAndStyles":"Scripts et styles","jsLibrary":"Bibliothèque JavaScript"},"editorTutorials":{"component":"Composant","componentContent":"Le panneau des composants de droite te propose de nombreux blocs d'application (composants) prêts à l'emploi. Ceux-ci peuvent être glissés sur le canevas pour être utilisés. Tu peux aussi créer tes propres composants avec quelques connaissances en codage.","canvas":"Toile","canvasContent":"Construis tes applications sur le Canvas avec une approche \"Ce que tu vois est ce que tu obtiens\". Il te suffit de faire glisser et de déposer les composants pour concevoir ta mise en page, et d'utiliser les raccourcis clavier pour une édition rapide comme supprimer, copier et coller. Une fois qu'un composant est sélectionné, tu peux peaufiner chaque détail, du style et de la mise en page à la liaison des données et au comportement logique. De plus, tu bénéficies de l'avantage supplémentaire de la conception réactive, ce qui garantit que tes applications sont superbes sur n'importe quel appareil.","queryData":"Interroger les données","queryDataContent":"Tu peux créer des requêtes de données ici et te connecter à tes sources de données MySQL, MongoDB, Redis, Airtable et bien d'autres. Après avoir configuré la requête, clique sur \"Exécuter\" pour obtenir les données et poursuivre le tutoriel.","compProperties":"Propriétés des composants"},"homeTutorials":{"createAppContent":"🎉 Bienvenue sur {nom du produit}, clique sur \\'App\\' et commence à créer ta première application.","createAppTitle":"Créer une application"},"history":{"layout":"\\Ajustement de la mise en page \"{0}\\\".","upgrade":"Mise à niveau \"{0}\".","delete":"Supprimer \\N'{0}\\'","add":"Ajouter \"{0}\\","modify":"Modifier \\N- \"{0}\\N","rename":"Renomme \\N\"{1}\\N\" en \\N\"{0}\\N\".","recover":"Récupérer la version \"{2}\".","recoverVersion":"Récupérer la version","andSoOn":"et ainsi de suite","timeFormat":"MM DD à hh:mm A","emptyHistory":"Pas d'antécédents","currentVersionWithBracket":" (Actuel)","currentVersion":"Version actuelle","justNow":"Tout de suite","history":"Histoire"},"home":{"allApplications":"Toutes les applications","allModules":"Tous les modules","allFolders":"Tous les dossiers","modules":"Modules","module":"Module","trash":"Poubelle","queryLibrary":"Bibliothèque de requêtes","datasource":"Sources de données","selectDatasourceType":"Sélectionne le type de source de données","home":"Accueil | Zone d'administration","all":"Tous","app":"App","navigation":"Navigation","navLayout":"Navigation PC","navLayoutDesc":"Menu à gauche pour faciliter la navigation sur le bureau.","mobileTabLayout":"Navigation mobile","mobileTabLayoutDesc":"Barre de navigation inférieure pour une navigation mobile fluide.","folders":"Dossiers","folder":"Dossier","rootFolder":"Racine","import":"Importation","export":"Exporter vers JSON","inviteUser":"Invite les membres","createFolder":"Créer un dossier","createFolderSubTitle":"Nom du dossier :","moveToFolder":"Déplacer vers le dossier","moveToTrash":"Déplacer vers la poubelle","moveToFolderSubTitle":"Déplace-toi vers :","folderName":"Nom du dossier :","resCardSubTitle":"{heure} par {créateur}","trashEmpty":"La poubelle est vide.","projectEmpty":"Il n'y a rien ici.","projectEmptyCanAdd":"Tu n'as pas encore d'application. Clique sur Nouveau pour commencer.","name":"Nom","type":"Type","creator":"Créé par","lastModified":"Dernière modification","deleteTime":"Effacer l'heure","createTime":"Créer du temps","datasourceName":"Nom de la source de données","databaseName":"Nom de la base de données","nameCheckMessage":"Le nom ne peut pas être vide","deleteElementTitle":"Supprimer définitivement","moveToTrashSubTitle":"{type} {nom} sera déplacé dans la corbeille.","deleteElementSubTitle":"Supprimer {type} {nom} définitivement, il ne peut pas être récupéré.","deleteSuccessMsg":"Supprimé avec succès","deleteErrorMsg":"Erreur supprimée","recoverSuccessMsg":"Récupéré avec succès","newDatasource":"Nouvelle source de données","creating":"Créer...","chooseDataSourceType":"Choisis le type de source de données","folderAlreadyExists":"Le dossier existe déjà","newNavLayout":"{nom d'utilisateur}\\'s {name} ","newApp":"Le nouveau {nom d'utilisateur}\\Nest le nouveau {nom}. ","importError":"Erreur d'importation, {message}","exportError":"Erreur d'exportation, {message}","importSuccess":"Succès de l'importation","fileUploadError":"Erreur de téléchargement de fichier","fileFormatError":"Erreur de format de fichier","groupWithSquareBrackets":"[Groupe] ","allPermissions":"Propriétaire","shareLink":"Lien de partage : ","copyLink":"Copier le lien","appPublicMessage":"Rends l'application publique. Tout le monde peut la consulter.","modulePublicMessage":"Rends le module public. Tout le monde peut le consulter.","memberPermissionList":"Permissions aux membres : ","orgName":"{orgName} admins","addMember":"Ajouter des membres","addPermissionPlaceholder":"Saisis un nom pour rechercher des membres","searchMemberOrGroup":"Recherche des membres ou des groupes : ","addPermissionErrorMessage":"L'ajout d'une permission a échoué, {message}","copyModalTitle":"Clone-le","copyNameLabel":"{type} nom","copyModalfolderLabel":"Ajouter au dossier","copyNamePlaceholder":"Saisis un nom de {type}","chooseNavType":"Choisis le type de navigation","createNavigation":"Créer une navigation"},"carousel":{"dotPosition":"Position des points de navigation","autoPlay":"AutoPlay","showDots":"Afficher les points de navigation"},"npm":{"invalidNpmPackageName":"Nom ou URL de paquetage npm invalide.","pluginExisted":"Ce plugin npm existait déjà","compNotFound":"Le composant {compName} n'a pas été trouvé.","addPluginModalTitle":"Ajouter un plugin à partir d'un dépôt npm","pluginNameLabel":"URL ou nom du package npm","noCompText":"Pas de composants.","compsLoading":"Chargement...","removePluginBtnText":"Enlever","addPluginBtnText":"Ajouter un plugin npm"},"toggleButton":{"valueDesc":"La valeur par défaut du bouton à bascule, par exemple : Faux","trueDefaultText":"Cache-toi","falseDefaultText":"Montrer","trueLabel":"Texte pour True","falseLabel":"Texte pour Faux","trueIconLabel":"Icône pour True","falseIconLabel":"Icône pour Faux","iconPosition":"Position de l'icône","showText":"Afficher le texte","alignment":"Alignement","showBorder":"Afficher la bordure"},"componentDoc":{"markdownDemoText":"**Lowcoder** | Crée des applications logicielles pour ton entreprise et tes clients avec un minimum d'expérience en matière de codage. Lowcoder est la meilleure alternative à Retool, Appsmith ou Tooljet.","demoText":"Lowcoder | Crée des applications logicielles pour ton entreprise et tes clients avec une expérience minimale du codage. Lowcoder est la meilleure alternative à Retool, Appsmith ou Tooljet.","submit":"Soumettre","style":"Style","danger":"Danger","warning":"Avertissement","success":"Succès","menu":"Menu","link":"Lien","customAppearance":"Apparence personnalisée","search":"Recherche","pleaseInputNumber":"Saisis un numéro","mostValue":"La plus grande valeur","maxRating":"Valeur nominale maximale","notSelect":"Non sélectionné","halfSelect":"Demi-sélection","pleaseSelect":"Choisis","title":"Titre","content":"Contenu","componentNotFound":"Le composant n'existe pas","example":"Exemples","defaultMethodDesc":"Définir la valeur de la propriété {nom}","propertyUsage":"Tu peux lire les informations relatives aux composants en accédant aux propriétés des composants par leur nom partout où tu peux écrire du JavaScript.","property":"Propriétés","propertyName":"Nom de la propriété","propertyType":"Type","propertyDesc":"Description","event":"Événements","eventName":"Nom de l'événement","eventDesc":"Description","mehtod":"Méthodes","methodUsage":"Tu peux interagir avec les composants par le biais de leurs méthodes et tu peux les appeler par leur nom partout où tu peux écrire du JavaScript. Tu peux aussi les appeler par le biais de l'action \"Composant de contrôle\" d'un événement.","methodName":"Nom de la méthode","methodDesc":"Description","showBorder":"Afficher la bordure","haveTry":"Essaie toi-même","settings":"Réglage","settingValues":"Valeur de réglage","defaultValue":"Valeur par défaut","time":"L'heure","date":"Date","noValue":"Aucun","xAxisType":"Type d'axe X","hAlignType":"Alignement horizontal","leftLeftAlign":"Alignement gauche-gauche","leftRightAlign":"Alignement gauche-droite","topLeftAlign":"Alignement haut-gauche","topRightAlign":"Alignement haut-droit","validation":"Validation","required":"Exigée","defaultStartDateValue":"Date de début par défaut","defaultEndDateValue":"Date de fin par défaut","basicUsage":"Utilisation de base","basicDemoDescription":"Les exemples suivants montrent l'utilisation de base du composant.","noDefaultValue":"Pas de valeur par défaut","forbid":"Interdit","placeholder":"Placeholder","pleaseInputPassword":"Saisis ton mot de passe","password":"Mot de passe","textAlign":"Alignement du texte","length":"Longueur","top":"Haut","pleaseInputName":"Saisis ton nom","userName":"Nom","fixed":"Fixe","responsive":"Réactif","workCount":"Nombre de mots","cascaderOptions":"Options de cascade","pleaseSelectCity":"Choisis une ville","advanced":"Avancé","showClearIcon":"Afficher l'icône d'effacement","likedFruits":"Favoris","option":"Option","singleFileUpload":"Téléchargement d'un seul fichier","multiFileUpload":"Téléchargement de plusieurs fichiers","folderUpload":"Téléchargement de dossier","multiFile":"Fichiers multiples","folder":"Dossier","open":"Ouvrir","favoriteFruits":"Fruits préférés","pleaseSelectOneFruit":"Choisis un fruit","notComplete":"Pas complet","complete":"Complète","echart":"Diagramme","lineChart":"Graphique en ligne","basicLineChart":"Diagramme linéaire de base","lineChartType":"Type de graphique linéaire","stackLineChart":"Ligne empilée","areaLineChart":"Ligne de surface","scatterChart":"Diagramme de dispersion","scatterShape":"Forme de l'éparpillement","scatterShapeCircle":"Cercle","scatterShapeRect":"Rectangle","scatterShapeTri":"Triangle","scatterShapeDiamond":"Diamant","scatterShapePin":"Épingle à cheveux","scatterShapeArrow":"Flèche","pieChart":"Diagramme circulaire","basicPieChart":"Diagramme circulaire de base","pieChatType":"Type de diagramme à secteurs","pieChartTypeCircle":"Tableau des beignets","pieChartTypeRose":"Tableau des roses","titleAlign":"Titre Position","color":"Couleur","dashed":"En pointillé","imADivider":"Je suis une ligne de démarcation","tableSize":"Taille de la table","subMenuItem":"Sous-menu {num}","menuItem":"Menu {num}","labelText":"Étiquette","labelPosition":"Étiquette - Position","labelAlign":"Étiquette - Aligner","optionsOptionType":"Méthode de configuration","styleBackgroundColor":"Couleur de fond","styleBorderColor":"Couleur de la bordure","styleColor":"Couleur de la police","selectionMode":"Mode de sélection des rangées","paginationSetting":"Réglage de la pagination","paginationShowSizeChanger":"Aide les utilisateurs à modifier le nombre d'entrées par page","paginationShowSizeChangerButton":"Bouton de changement de taille","paginationShowQuickJumper":"Show Quick Jumper","paginationHideOnSinglePage":"Cacher lorsqu'il n'y a qu'une seule page","paginationPageSizeOptions":"Taille de la page","chartConfigCompType":"Type de graphique","xConfigType":"Type d'axe X","loading":"Chargement","disabled":"Désactivé","minLength":"Longueur minimale","maxLength":"Longueur maximale","showCount":"Afficher le nombre de mots","autoHeight":"Hauteur","thousandsSeparator":"Séparateur de milliers","precision":"Places décimales","value":"Valeur par défaut","formatter":"Format","min":"Valeur minimale","max":"Valeur maximale","step":"Taille de l'étape","start":"Heure de début","end":"L'heure de la fin","allowHalf":"Autoriser la sélection de la moitié","filetype":"Type de fichier","showUploadList":"Afficher la liste des téléchargements","uploadType":"Type de téléchargement","allowClear":"Afficher l'icône d'effacement","minSize":"Taille minimale du fichier","maxSize":"Taille maximale du fichier","maxFiles":"Nombre maximum de fichiers téléchargés","format":"Format","minDate":"Date minimum","maxDate":"Date maximale","minTime":"Durée minimale","maxTime":"Durée maximale","text":"Texte","type":"Type","hideHeader":"Cacher l'en-tête","hideBordered":"Cacher la bordure","src":"URL de l'image","showInfo":"Valeur d'affichage","mode":"Mode","onlyMenu":"Menu unique","horizontalAlignment":"Alignement horizontal","row":"Gauche","column":"Haut","leftAlign":"Alignement à gauche","rightAlign":"Alignement droit","percent":"Pourcentage","fixedHeight":"Hauteur fixe","auto":"Adaptatif","directory":"Dossier","multiple":"Fichiers multiples","singleFile":"Fichier unique","manual":"Manuel","default":"Défaut","small":"Petit","middle":"Moyen","large":"Grandes","single":"Célibataire","multi":"Multiple","close":"Fermer","ui":"Mode UI","line":"Graphique en ligne","scatter":"Diagramme de dispersion","pie":"Diagramme circulaire","basicLine":"Diagramme linéaire de base","stackedLine":"Tableau à lignes empilées","areaLine":"Zone Carte de la zone","basicPie":"Diagramme circulaire de base","doughnutPie":"Tableau des beignets","rosePie":"Tableau des roses","category":"Catégorie Axe","circle":"Cercle","rect":"Rectangle","triangle":"Triangle","diamond":"Diamant","pin":"Épingle à cheveux","arrow":"Flèche","left":"Gauche","right":"Droit","center":"Centre","bottom":"Le fond","justify":"Justifie les deux extrémités"},"playground":{"url":"https://app.lowcoder.cloud/playground/{compType}/1","data":"État actuel des données","preview":"Aperçu","property":"Propriétés","console":"Console Visual Script","executeMethods":"Exécuter les méthodes","noMethods":"Pas de méthodes.","methodParams":"Paramètres de la méthode","methodParamsHelp":"Paramètres de la méthode d'entrée à l'aide de JSON. Par exemple, tu peux définir les paramètres de la méthode avec : [1] ou 1"},"calendar":{"headerBtnBackground":"Arrière-plan des boutons","btnText":"Texte du bouton","title":"Titre","selectBackground":"Sélection d'antécédents"},"componentDocExtra":{"table":"Documentation supplémentaire pour le composant tableau"},"idSource":{"title":"Fournisseurs OAuth","form":"Courriel","pay":"Premium","enable":"Activer","unEnable":"Non activé","loginType":"Type de connexion","status":"Statut","desc":"Description","manual":"Carnet d'adresses :","syncManual":"Synchroniser le carnet d'adresses","syncManualSuccess":"Synchronisation réussie","enableRegister":"Autoriser l'enregistrement","saveBtn":"Sauvegarder et activer","save":"Sauvegarde","none":"Aucun","formPlaceholder":"Saisis {label}","formSelectPlaceholder":"Choisis le {label}","saveSuccess":"Sauvegardé avec succès","dangerLabel":"Zone de danger","dangerTip":"La désactivation de ce fournisseur d'identifiants peut entraîner l'impossibilité pour certains utilisateurs de se connecter. Procède avec prudence.","disable":"Désactiver","disableSuccess":"Désactivé avec succès","encryptedServer":"-------- Crypté du côté du serveur --------","disableTip":"Conseils","disableContent":"La désactivation de ce fournisseur d'identifiants peut entraîner l'impossibilité pour certains utilisateurs de se connecter. Es-tu sûr de vouloir continuer ?","manualTip":"","lockTip":"Le contenu est verrouillé. Pour apporter des modifications, clique sur l'icône pour le déverrouiller.","lockModalContent":"La modification du champ \"ID Attribute\" peut avoir des conséquences importantes sur l'identification de l'utilisateur. Confirme que tu comprends les implications de ce changement avant de continuer.","payUserTag":"Premium"},"slotControl":{"configSlotView":"Configurer la vue de l'emplacement"},"jsonLottie":{"lottieJson":"Lottie JSON","speed":"La vitesse","width":"Largeur","height":"Hauteur","backgroundColor":"Couleur de fond","animationStart":"Début de l'animation","valueDesc":"Données JSON actuelles","loop":"Boucle","auto":"Auto","onHover":"Au survol","singlePlay":"Jeu unique","endlessLoop":"Boucle sans fin","keepLastFrame":"Maintien de l'affichage de la dernière image"},"timeLine":{"titleColor":"Titre Couleur","subTitleColor":"Couleur des sous-titres","lableColor":"Couleur de l'étiquette","value":"Données chronologiques","mode":"Ordre d'affichage","left":"Droit au contenu","right":"Contenu à gauche","alternate":"Ordre alternatif du contenu","modeTooltip":"Régler le contenu pour qu'il apparaisse à gauche/droite ou alternativement des deux côtés de la ligne de temps","reverse":"Les événements les plus récents d'abord","pending":"Texte du nœud en attente","pendingDescription":"Lorsque cette option est activée, un dernier nœud avec le texte et un indicateur d'attente s'affichent.","defaultPending":"Amélioration continue","clickTitleEvent":"Clique sur Titre de l'événement","clickTitleEventDesc":"Clique sur Titre de l'événement","Introduction":"Introduction Clés","helpTitle":"Titre de la ligne du temps (obligatoire)","helpsubTitle":"Sous-titre de la chronologie","helpLabel":"Étiquette de la ligne de temps, utilisée pour afficher les dates","helpColor":"Indique la couleur du nœud de la ligne de temps","helpDot":"Rendre les nœuds de la ligne de temps sous forme d'icônes Ant Design","helpTitleColor":"Contrôle individuellement la couleur du titre du nœud","helpSubTitleColor":"Contrôle individuellement la couleur du sous-titre du nœud","helpLableColor":"Contrôle individuellement la couleur de l'icône du nœud","valueDesc":"Données de la chronologie","clickedObjectDesc":"Données de l'élément cliqué","clickedIndexDesc":"Index des éléments cliqués"},"comment":{"value":"Données de la liste de commentaires","showSendButton":"Autoriser les commentaires","title":"Titre","titledDefaultValue":"%d Commentaire au total","placeholder":"Shift + Enter pour commenter ; Saisis @ ou # pour une saisie rapide","placeholderDec":"Placeholder","buttonTextDec":"Titre du bouton","buttonText":"Commentaire","mentionList":"Données de la liste des mentions","mentionListDec":"Mots clés ; Données de la liste des mentions de valeur","userInfo":"Info utilisateur","dateErr":"Erreur de date","commentList":"Liste des commentaires","deletedItem":"Point supprimé","submitedItem":"Article soumis","deleteAble":"Afficher le bouton de suppression","Introduction":"Introduction Clés","helpUser":"Informations sur l'utilisateur (obligatoire)","helpname":"Nom d'utilisateur (obligatoire)","helpavatar":"URL de l'avatar (Haute priorité)","helpdisplayName":"Nom d'affichage (faible priorité)","helpvalue":"Contenu des commentaires","helpcreatedAt":"Créer une date"},"mention":{"mentionList":"Données de la liste des mentions"},"autoComplete":{"value":"Auto Complete Value","checkedValueFrom":"Valeur vérifiée De","ignoreCase":"Recherche Ignorer le cas","searchLabelOnly":"Recherche sur l'étiquette uniquement","searchFirstPY":"Rechercher le premier pinyin","searchCompletePY":"Rechercher le pinyin complet","searchText":"Texte de recherche","SectionDataName":"Données d'auto-complétion","valueInItems":"Valeur en articles","type":"Type","antDesign":"AntDesign","normal":"Normal","selectKey":"Clé","selectLable":"Étiquette","ComponentType":"Type de composant","colorIcon":"Bleu","grewIcon":"Gris","noneIcon":"Aucun","small":"Petit","large":"Grandes","componentSize":"Taille du composant","Introduction":"Introduction Clés","helpLabel":"Étiquette","helpValue":"Valeur"},"responsiveLayout":{"column":"Colonnes","atLeastOneColumnError":"La mise en page responsive conserve au moins une colonne","columnsPerRow":"Colonnes par ligne","columnsSpacing":"Espacement des colonnes (px)","horizontal":"Horizontal","vertical":"Vertical","mobile":"Mobile","tablet":"Tablette","desktop":"Bureau","rowStyle":"Style de la rangée","columnStyle":"Style de colonne","minWidth":"Min. Largeur","rowBreak":"Pause dans les rangs","matchColumnsHeight":"Hauteur des colonnes","rowLayout":"Disposition des rangées","columnsLayout":"Disposition des colonnes"},"navLayout":{"mode":"Mode","modeInline":"En ligne","modeVertical":"Vertical","width":"Largeur","widthTooltip":"Pixel ou pourcentage, par exemple 520, 60%","navStyle":"Style de menu","navItemStyle":"Style de l'élément de menu"}} \ No newline at end of file +{"productName":"Lowcoder","productDesc":"Crée des applications logicielles pour ton entreprise et tes clients avec un minimum d'expérience en matière de codage. Lowcoder est une excellente alternative à Retool, Appsmith et Tooljet.","notSupportedBrowser":"Ton navigateur actuel peut avoir des problèmes de compatibilité. Pour une expérience utilisateur optimale, utilise la dernière version de Chrome.","create":"Créer","move":"Déplacer","addItem":"Ajouter","newItem":"Nouveau","copy":"Copie","rename":"Renommer","delete":"Supprimer","deletePermanently":"Supprimer définitivement","remove":"Enlever","recover":"Récupérer","edit":"Éditer","view":"Voir","value":"Valeur","data":"Données","information":"Informations","success":"Succès","warning":"Avertissement","error":"Erreur","reference":"Référence","text":"Texte","label":"Étiquette","color":"Couleur","form":"Formulaire","menu":"Menu","menuItem":"Point de menu","ok":"OK","cancel":"Annuler","finish":"Finir","reset":"Remise à zéro","icon":"Icône","code":"Code","title":"Titre","emptyContent":"Contenu vide","more":"Plus d'informations","search":"Recherche","back":"Retour","accessControl":"Contrôle d'accès","copySuccess":"Copié avec succès","copyError":"Erreur de copie","api":{"publishSuccess":"Publié avec succès","recoverFailed":"Échec de la récupération","needUpdate":"Ta version actuelle est obsolète. Mets-toi à jour avec la dernière version."},"codeEditor":{"notSupportAutoFormat":"L'éditeur de code actuel ne prend pas en charge le formatage automatique.","fold":"Plier"},"exportMethod":{"setDesc":"Définir la propriété : {propriété}","clearDesc":"Efface la propriété : {propriété}","resetDesc":"Réinitialiser la propriété : {propriété} à la valeur par défaut"},"method":{"focus":"Définir l'objectif","focusOptions":"Options de mise au point. Voir HTMLElement.focus()","blur":"Enlever la mise au point","click":"Clique sur","select":"Sélectionner tout le texte","setSelectionRange":"Définir les positions de début et de fin de la sélection de texte","selectionStart":"Index basé sur 0 du premier caractère sélectionné","selectionEnd":"Index basé sur 0 du caractère après le dernier caractère sélectionné","setRangeText":"Remplacer la plage de texte","replacement":"Chaîne à insérer","replaceStart":"Index basé sur 0 du premier caractère à remplacer","replaceEnd":"Index basé sur 0 du caractère après le dernier caractère à remplacer"},"errorBoundary":{"encounterError":"Le chargement du composant a échoué. Vérifie ta configuration.","clickToReload":"Cliquer pour recharger","errorMsg":"Erreur : "},"imgUpload":{"notSupportError":"Prend en charge uniquement les types d'images {types}","exceedSizeError":"La taille de l'image ne doit pas dépasser {taille}"},"gridCompOperator":{"notSupport":"Non pris en charge","selectAtLeastOneComponent":"Choisis au moins une composante","selectCompFirst":"Sélectionne les composants avant de les copier","noContainerSelected":"[Bug] Aucun conteneur n'est sélectionné","deleteCompsSuccess":"Supprimé avec succès. Appuie sur la touche {undoKey} pour annuler.","deleteCompsTitle":"Supprimer des composants","deleteCompsBody":"Es-tu sûr de vouloir supprimer les composants sélectionnés {compNum} ?","cutCompsSuccess":"Coupe avec succès. Appuie sur {pasteKey} pour coller, ou sur {undoKey} pour annuler."},"leftPanel":{"queries":"Requêtes de données dans ton application","globals":"Variables de données globales","propTipsArr":"Éléments {num}","propTips":"{num} Clés","propTipArr":"Item {num}","propTip":"{num} Clé","stateTab":"État","settingsTab":"Réglages","toolbarTitle":"L'individualisation","toolbarPreload":"Scripts et styles","components":"Composants actifs","modals":"Modaux in-App","expandTip":"Cliquer pour développer les données de {composant}\\N","collapseTip":"Cliquer pour réduire les données de {composant}\\N"},"bottomPanel":{"title":"Requêtes de données","run":"Exécuter","noSelectedQuery":"Aucune requête sélectionnée","metaData":"Métadonnées de la source de données","noMetadata":"Pas de métadonnées disponibles","metaSearchPlaceholder":"Recherche de métadonnées","allData":"Toutes les tables"},"rightPanel":{"propertyTab":"Propriétés","noSelectedComps":"Aucun composant n'est sélectionné. Clique sur un composant pour afficher ses propriétés.","createTab":"Insérer","searchPlaceHolder":"Rechercher des composants ou des modules","uiComponentTab":"Composants","extensionTab":"Extensions","modulesTab":"Modules","moduleListTitle":"Modules","pluginListTitle":"Plugins","emptyModules":"Les modules sont des applications Mikro-Apps réutilisables. Tu peux les intégrer dans ton application.","searchNotFound":"Tu ne trouves pas le bon composant ? Soumettre un problème","emptyPlugins":"Aucun plugin n'a été ajouté","contactUs":"Nous contacter","issueHere":"ici."},"prop":{"expand":"Élargir","columns":"Colonnes","videokey":"Clé vidéo","rowSelection":"Sélection des rangs","toolbar":"Barre d'outils","pagination":"Pagination","logo":"Logo","style":"Style","inputs":"Entrées","meta":"Métadonnées","data":"Données","hide":"Cache-toi","loading":"Chargement","disabled":"Désactivé","placeholder":"Placeholder","showClear":"Afficher le bouton d'effacement","showSearch":"Recherche possible","defaultValue":"Valeur par défaut","required":"Champ obligatoire","readOnly":"Lecture seule","readOnlyTooltip":"Les composants en lecture seule apparaissent normaux mais ne peuvent pas être modifiés.","minimum":"Minimum","maximum":"Maximum","regex":"Regex","minLength":"Longueur minimale","maxLength":"Longueur maximale","height":"Hauteur","width":"Largeur","selectApp":"Sélectionne l'application","showCount":"Afficher le décompte","textType":"Type de texte","customRule":"Règle personnalisée","customRuleTooltip":"Une chaîne non vide indique une erreur ; une chaîne vide ou nulle signifie que la validation est réussie. Exemple : ","manual":"Manuel","map":"Carte","json":"JSON","use12Hours":"Utilise le format 12 heures","hourStep":"Heure Étape","minuteStep":"Pas de minute","secondStep":"Deuxième étape","minDate":"Date minimum","maxDate":"Date maximale","minTime":"Durée minimale","maxTime":"Durée maximale","type":"Type","showLabel":"Afficher l'étiquette","showHeader":"Afficher l'en-tête","showBody":"Montrer le corps","showFooter":"Afficher le pied de page","maskClosable":"Clique sur Extérieur pour fermer","showMask":"Afficher le masque"},"autoHeightProp":{"auto":"Auto","fixed":"Fixe"},"labelProp":{"text":"Étiquette","tooltip":"Info-bulle","position":"Position","left":"Gauche","top":"Haut","align":"Alignement","width":"Largeur","widthTooltip":"La largeur de l'étiquette prend en charge les pourcentages (%) et les pixels (px)."},"eventHandler":{"eventHandlers":"Gestionnaires d'événements","emptyEventHandlers":"Pas de gestionnaire d'événements","incomplete":"Sélection incomplète","inlineEventTitle":"Sur {nom de l'événement}","event":"Événement","action":"Action","noSelect":"Pas de sélection","runQuery":"Exécuter une requête de données","selectQuery":"Sélectionner une requête de données","controlComp":"Contrôler un composant","runScript":"Exécuter JavaScript","runScriptPlaceHolder":"Inscris le code ici","component":"Composant","method":"Méthode","setTempState":"Définir une valeur d'état temporaire","state":"État","triggerModuleEvent":"Déclencher un événement de module","moduleEvent":"Événement du module","goToApp":"Aller à une autre application","queryParams":"Paramètres de la requête","hashParams":"Paramètres de hachage","showNotification":"Afficher une notification","text":"Texte","level":"Niveau","duration":"Durée","notifyDurationTooltip":"L'unité de temps peut être 's' (seconde, par défaut) ou 'ms' (milliseconde). La durée maximale est de {max} secondes","goToURL":"Ouvrir une URL","openInNewTab":"Ouvrir dans un nouvel onglet","copyToClipboard":"Copier une valeur dans le presse-papiers","copyToClipboardValue":"Valeur","export":"Exportation de données","exportNoFileType":"Pas de sélection (optionnel)","fileName":"Nom du fichier","fileNameTooltip":"Inclure l'extension pour spécifier le type de fichier, par exemple, \\N \"image.png\\\".","fileType":"Type de fichier","condition":"Ne cours que lorsque...","conditionTooltip":"Ne lance le gestionnaire d'événement que lorsque cette condition est évaluée à 'vrai'.","debounce":"Debounce pour","throttle":"L'accélérateur pour","slowdownTooltip":"Utilise la fonction debounce ou throttle pour contrôler la fréquence des déclenchements d'action. L'unité de temps peut être \\'ms\\' (milliseconde, par défaut) ou \\'s\\' (seconde).","notHandledError":"Non traité","currentApp":"Actuel"},"event":{"submit":"Soumettre","submitDesc":"Déclencheurs à la soumission","change":"Changer","changeDesc":"Déclencheurs sur les changements de valeur","focus":"Focus","focusDesc":"Déclencheurs sur la focalisation","blur":"Flou","blurDesc":"Déclencheurs sur le flou","click":"Clique sur","clickDesc":"Déclencheurs au clic","close":"Fermer","closeDesc":"Déclencheurs à la fermeture","parse":"Analyser","parseDesc":"Déclencheurs sur Parse","success":"Succès","successDesc":"Déclencheurs de succès","delete":"Supprimer","deleteDesc":"Déclencheurs de suppression","mention":"Mention","mentionDesc":"Déclencheurs à la mention"},"themeDetail":{"primary":"Couleur de la marque","primaryDesc":"Couleur primaire par défaut utilisée par la plupart des composants.","textDark":"Couleur foncée du texte","textDarkDesc":"Utilisé lorsque la couleur d'arrière-plan est claire","textLight":"Couleur de texte claire","textLightDesc":"Utilisé lorsque la couleur d'arrière-plan est sombre","canvas":"Couleur de la toile","canvasDesc":"Couleur d'arrière-plan par défaut de l'application","primarySurface":"Couleur du conteneur","primarySurfaceDesc":"Couleur d'arrière-plan par défaut pour les composants tels que les tableaux","borderRadius":"Rayon de la bordure","borderRadiusDesc":"Rayon de bordure par défaut utilisé par la plupart des composants","chart":"Style de graphique","chartDesc":"Entrée pour Echarts","echartsJson":"Thème JSON","margin":"Marge","marginDesc":"Marge par défaut généralement utilisée pour la plupart des composants.","padding":"Rembourrage","paddingDesc":"Rembourrage par défaut généralement utilisé pour la plupart des composants.","containerHeaderPadding":"Rembourrage de l'en-tête","containerheaderpaddingDesc":"Rembourrage d'en-tête par défaut généralement utilisé pour la plupart des composants.","gridColumns":"Colonnes de la grille","gridColumnsDesc":"Nombre de colonnes par défaut généralement utilisé pour la plupart des conteneurs."},"style":{"resetTooltip":"Réinitialiser les styles. Efface le champ de saisie pour réinitialiser un style individuel.","textColor":"Couleur du texte","contrastText":"Contraste Couleur du texte","generated":"Généré","customize":"Personnaliser","staticText":"Texte statique","accent":"Accent","validate":"Message de validation","border":"Couleur de la bordure","borderRadius":"Rayon de la bordure","borderWidth":"Largeur de la bordure","background":"Contexte","headerBackground":"Arrière-plan de l'en-tête","footerBackground":"Arrière-plan du pied de page","fill":"Remplir","track":"Poursuivre","links":"Liens","thumb":"Pouce","thumbBorder":"Bordure du pouce","checked":"Vérifié","unchecked":"Non vérifié","handle":"Poignée","tags":"Tags","tagsText":"Texte des étiquettes","multiIcon":"Icône multi-sélection","tabText":"Texte de l'onglet","tabAccent":"Onglet Accent","checkedBackground":"Arrière-plan vérifié","uncheckedBackground":"Arrière-plan non vérifié","uncheckedBorder":"Bordure non vérifiée","indicatorBackground":"Contexte de l'indicateur","tableCellText":"Texte de la cellule","selectedRowBackground":"Arrière-plan de la rangée sélectionnée","hoverRowBackground":"Arrière-plan de la rangée de survol","alternateRowBackground":"Autre arrière-plan de rangée","tableHeaderBackground":"Arrière-plan de l'en-tête","tableHeaderText":"Texte de l'en-tête","toolbarBackground":"Arrière-plan de la barre d'outils","toolbarText":"Texte de la barre d'outils","pen":"Stylo","footerIcon":"Icône de bas de page","tips":"Conseils","margin":"Marge","padding":"Rembourrage","marginLeft":"Marge gauche","marginRight":"Marge droite","marginTop":"Marge supérieure","marginBottom":"Marge inférieure","containerHeaderPadding":"Rembourrage de l'en-tête","containerFooterPadding":"Remplissage du pied de page","containerBodyPadding":"Rembourrage du corps","minWidth":"Largeur minimale","aspectRatio":"Rapport d'aspect","textSize":"Taille du texte"},"export":{"hiddenDesc":"Si c'est vrai, le composant est caché","disabledDesc":"Si c'est vrai, le composant est désactivé et non interactif","visibleDesc":"Si c'est vrai, le composant est visible","inputValueDesc":"Valeur actuelle de l'entrée","invalidDesc":"Indique si la valeur est invalide","placeholderDesc":"Texte de remplacement lorsqu'aucune valeur n'est définie","requiredDesc":"Si c'est vrai, une valeur valide est requise","submitDesc":"Soumettre le formulaire","richTextEditorValueDesc":"Valeur actuelle de l'éditeur","richTextEditorReadOnlyDesc":"Si c'est vrai, l'éditeur est en lecture seule","richTextEditorHideToolBarDesc":"Si c'est vrai, la barre d'outils est cachée","jsonEditorDesc":"Données JSON actuelles","sliderValueDesc":"Valeur actuellement sélectionnée","sliderMaxValueDesc":"Valeur maximale du curseur","sliderMinValueDesc":"Valeur minimale du curseur","sliderStartDesc":"Valeur du point de départ sélectionné","sliderEndDesc":"Valeur du point final sélectionné","ratingValueDesc":"Cote actuellement sélectionnée","ratingMaxDesc":"Valeur nominale maximale","datePickerValueDesc":"Date actuellement sélectionnée","datePickerFormattedValueDesc":"Date sélectionnée formatée","datePickerTimestampDesc":"Horodatage de la date sélectionnée","dateRangeStartDesc":"Date de début de l'intervalle","dateRangeEndDesc":"Date de fin de l'intervalle","dateRangeStartTimestampDesc":"Horodatage de la date de début","dateRangeEndTimestampDesc":"Horodatage de la date de fin","dateRangeFormattedValueDesc":"Plage de dates formatée","dateRangeFormattedStartValueDesc":"Date de début formatée","dateRangeFormattedEndValueDesc":"Date de fin formatée","timePickerValueDesc":"Heure actuellement sélectionnée","timePickerFormattedValueDesc":"Formaté l'heure sélectionnée","timeRangeStartDesc":"Heure de début de la plage","timeRangeEndDesc":"Heure de fin de la plage","timeRangeFormattedValueDesc":"Formatage de l'intervalle de temps","timeRangeFormattedStartValueDesc":"Heure de début formatée","timeRangeFormattedEndValueDesc":"Heure de fin formatée"},"validationDesc":{"email":"Saisis une adresse électronique valide","url":"Saisis une URL valide","regex":"Tu dois correspondre au modèle spécifié","maxLength":"Trop de caractères, actuel : {longueur}, maximum : {maxLength}","minLength":"Pas assez de caractères, actuel : {longueur}, minimum : {minLength}","maxValue":"La valeur dépasse le maximum, courant : {valeur}, maximum : {max}","minValue":"Valeur inférieure au minimum, courant : {valeur}, minimum : {min}","maxTime":"Le temps dépasse le maximum, actuel : {heure}, maximum : {maxTime}","minTime":"Temps inférieur au minimum, actuel : {time}, minimum : {minTime}","maxDate":"La date dépasse le maximum, courant : {date}, maximum : {maxDate}","minDate":"Date inférieure au minimum, courant : {date}, minimum : {minDate}"},"query":{"noQueries":"Aucune requête de données n'est disponible.","queryTutorialButton":"Voir les documents {valeur}","datasource":"Tes sources de données","newDatasource":"Nouvelle source de données","generalTab":"Généralités","notificationTab":"Notification","advancedTab":"Avancé","showFailNotification":"Afficher une notification en cas d'échec","failCondition":"Conditions de défaillance","failConditionTooltip1":"Personnalise les conditions de défaillance et les notifications correspondantes.","failConditionTooltip2":"Si l'une des conditions revient vraie, la requête est marquée comme ayant échoué et déclenche la notification correspondante.","showSuccessNotification":"Afficher la notification en cas de succès","successMessageLabel":"Message de réussite","successMessage":"Exécution réussie","notifyDuration":"Durée","notifyDurationTooltip":"Durée de la notification. L'unité de temps peut être \\Ns (seconde, par défaut) ou \\Nms (milliseconde). La valeur par défaut est {default}s. La valeur maximale est {max}s.","successMessageWithName":"{nom} exécution réussie","failMessageWithName":"L'exécution de {nom} a échoué : {résultat}","showConfirmationModal":"Afficher la fenêtre de confirmation avant l'exécution","confirmationMessageLabel":"Message de confirmation","confirmationMessage":"Es-tu sûr de vouloir exécuter cette requête de données ?","newQuery":"Nouvelle requête de données","newFolder":"Nouveau dossier","recentlyUsed":"Récemment utilisé","folder":"Dossier","folderNotEmpty":"Le dossier n'est pas vide","dataResponder":"Répondant aux données","tempState":"État temporaire","transformer":"Transformateur","quickRestAPI":"Requête REST","quickStreamAPI":"Requête de flux","quickGraphql":"Requête GraphQL","lowcoderAPI":"API Lowcoder","executeJSCode":"Exécuter le code JavaScript","importFromQueryLibrary":"Importer à partir de la bibliothèque de requêtes","importFromFile":"Importer à partir d'un fichier","triggerType":"Déclenché lorsque...","triggerTypeAuto":"Les entrées changent ou au chargement de la page","triggerTypePageLoad":"Lorsque l'application (la page) se charge","triggerTypeManual":"Seulement lorsque tu le déclenches manuellement","chooseDataSource":"Choisis la source de données","method":"Méthode","updateExceptionDataSourceTitle":"Mettre à jour une source de données défaillante","updateExceptionDataSourceContent":"Mets à jour la requête suivante avec la même source de données défaillante :","update":"Mise à jour","disablePreparedStatement":"Désactiver les déclarations préparées","disablePreparedStatementTooltip":"La désactivation des instructions préparées permet de générer du SQL dynamique, mais augmente le risque d'injection SQL","timeout":"Délai d'attente après","timeoutTooltip":"Unité par défaut : ms. Unités d'entrée prises en charge : ms, s. Valeur par défaut : {defaultSeconds} secondes. Valeur maximale : {maxSeconds} secondes. Par exemple, 300 (c'est-à-dire 300 ms), 800 ms, 5 s.","periodic":"Exécute périodiquement cette requête de données","periodicTime":"Période","periodicTimeTooltip":"Période entre deux exécutions successives. Unité par défaut : ms. Unités d'entrée prises en charge : ms, s. Valeur minimale : 100 ms. L'exécution périodique est désactivée pour les valeurs inférieures à 100ms. Par exemple, 300 (c'est-à-dire 300ms), 800ms, 5s.","cancelPrevious":"Ignorer les résultats des exécutions précédentes non terminées","cancelPreviousTooltip":"Si une nouvelle exécution est déclenchée, le résultat des exécutions précédentes non terminées sera ignoré si elles ne se sont pas terminées, et ces exécutions ignorées ne déclencheront pas la liste d'événements de la requête.","dataSourceStatusError":"Si une nouvelle exécution est déclenchée, le résultat des exécutions précédentes non terminées sera ignoré, et les exécutions ignorées ne déclencheront pas la liste d'événements de la requête.","success":"Succès","fail":"Échec","successDesc":"Déclenché lorsque l'exécution est réussie","failDesc":"Déclenché lorsque l'exécution échoue","fixedDelayError":"La requête n'a pas été exécutée","execSuccess":"Exécution réussie","execFail":"Échec de l'exécution","execIgnored":"Les résultats de cette requête ont été ignorés","deleteSuccessMessage":"Supprimé avec succès. Tu peux utiliser {undoKey} pour annuler","dataExportDesc":"Données obtenues par la requête actuelle","codeExportDesc":"Code d'état de la requête en cours","successExportDesc":"Si la requête actuelle a été exécutée avec succès","messageExportDesc":"Informations renvoyées par la requête en cours","extraExportDesc":"Autres données dans la requête actuelle","isFetchingExportDesc":"La requête actuelle est-elle dans la demande ?","runTimeExportDesc":"Temps d'exécution de la requête actuelle (ms)","latestEndTimeExportDesc":"Dernier temps d'exécution","triggerTypeExportDesc":"Type de déclencheur","chooseResource":"Choisis une ressource","createDataSource":"Créer une nouvelle source de données","editDataSource":"Éditer","datasourceName":"Nom","datasourceNameRuleMessage":"Saisis le nom de la source de données","generalSetting":"Paramètres généraux","advancedSetting":"Paramètres avancés","port":"Port","portRequiredMessage":"Saisis un port","portErrorMessage":"Saisis un port correct","connectionType":"Type de connexion","regular":"Régulière","host":"Hôte","hostRequiredMessage":"Saisis le nom de domaine ou l'adresse IP de l'hôte","userName":"Nom de l'utilisateur","password":"Mot de passe","encryptedServer":"-------- Crypté du côté du serveur --------","uriRequiredMessage":"Saisis un URI","urlRequiredMessage":"Saisis une URL","uriErrorMessage":"Saisis un URI correct","urlErrorMessage":"Saisis une URL correcte","httpRequiredMessage":"Saisis http:// ou https://","databaseName":"Nom de la base de données","databaseNameRequiredMessage":"Saisis un nom de base de données","useSSL":"Utiliser SSL","userNameRequiredMessage":"Saisis ton nom","passwordRequiredMessage":"Saisis ton mot de passe","authentication":"Authentification","authenticationType":"Type d'authentification","sslCertVerificationType":"Vérification des certificats SSL","sslCertVerificationTypeDefault":"Vérifier le certificat de l'autorité de certification","sslCertVerificationTypeSelf":"Vérifier le certificat auto-signé","sslCertVerificationTypeDisabled":"Désactivé","selfSignedCert":"Cert auto-signé","selfSignedCertRequireMsg":"Saisis ton certificat","enableTurnOffPreparedStatement":"Activer le basculement des instructions préparées pour les requêtes","enableTurnOffPreparedStatementTooltip":"Tu peux activer ou désactiver les instructions préparées dans l'onglet Avancé de la requête.","serviceName":"Nom du service","serviceNameRequiredMessage":"Saisis le nom de ton service","useSID":"Utiliser le SID","connectSuccessfully":"Connexion réussie","saveSuccessfully":"Sauvegardé avec succès","database":"Base de données","cloudHosting":"Lowcoder hébergé dans le nuage ne peut pas accéder aux services locaux en utilisant 127.0.0.1 ou localhost. Essaie de te connecter aux sources de données du réseau public ou utilise un proxy inverse pour les services privés.","notCloudHosting":"Pour le déploiement hébergé par docker, Lowcoder utilise des réseaux en pont, donc 127.0.0.1 et localhost ne sont pas valides pour les adresses d'hôtes. Pour accéder aux sources de données des machines locales, réfère-toi à","howToAccessHostDocLink":"Comment accéder à l'API/DB de l'hôte","returnList":"Retourner","chooseDatasourceType":"Choisis le type de source de données","viewDocuments":"Voir les documents","testConnection":"Connexion de test","save":"Sauvegarde","whitelist":"Liste d'admissibilité","whitelistTooltip":"Ajoute les adresses IP de Lowcoder\\ à la liste d'autorisation de ta source de données si nécessaire.","address":"Adresse : ","nameExists":"Le nom {nom} existe déjà","jsQueryDocLink":"A propos de JavaScript Query","dynamicDataSourceConfigLoadingText":"Chargement de la configuration de la source de données supplémentaire...","dynamicDataSourceConfigErrText":"Échec du chargement de la configuration de la source de données supplémentaire.","retry":"Réessayer"},"sqlQuery":{"keyValuePairs":"Paires clé-valeur","object":"Objet","allowMultiModify":"Permettre la modification de plusieurs rangs","allowMultiModifyTooltip":"Si cette option est sélectionnée, toutes les lignes qui remplissent les conditions sont prises en compte. Sinon, seule la première ligne remplissant les conditions est prise en compte.","array":"Array","insertList":"Liste d'insertion","insertListTooltip":"Valeurs insérées alors qu'elles n'existent pas","filterRule":"Règle de filtrage","updateList":"Liste des mises à jour","updateListTooltip":"Les valeurs mises à jour au fur et à mesure peuvent être remplacées par les mêmes valeurs de la liste d'insertion.","sqlMode":"Mode SQL","guiMode":"Mode GUI","operation":"Fonctionnement","insert":"Insérer","upsert":"Insérer, mais mettre à jour en cas de conflit","update":"Mise à jour","delete":"Supprimer","bulkInsert":"Insertion en vrac","bulkUpdate":"Mise à jour en vrac","table":"Tableau","primaryKeyColumn":"Colonne de clé primaire"},"EsQuery":{"rawCommand":"Commande brute","queryTutorialButton":"Voir les documents de l'API Elasticsearch","request":"Demande"},"googleSheets":{"rowIndex":"Index des rangs","spreadsheetId":"ID de la feuille de calcul","sheetName":"Nom de la feuille","readData":"Lire les données","appendData":"Ajouter une ligne","updateData":"Rangée de mise à jour","deleteData":"Supprimer une ligne","clearData":"Effacer la rangée","serviceAccountRequireMessage":"Saisis ton compte de service","ASC":"ASC","DESC":"DESC","sort":"Trier","sortPlaceholder":"Nom"},"queryLibrary":{"export":"Exporter vers JSON","noInput":"La requête actuelle n'a pas d'entrée","inputName":"Nom","inputDesc":"Description","emptyInputs":"Pas d'entrées","clickToAdd":"Ajouter","chooseQuery":"Choisis la requête","viewQuery":"Voir la requête","chooseVersion":"Choisis la version","latest":"Dernières nouvelles","publish":"Publie","historyVersion":"Version historique","deleteQueryLabel":"Supprimer une requête","deleteQueryContent":"La requête ne peut pas être récupérée après sa suppression. Effacer la requête ?","run":"Exécuter","readOnly":"Lecture seule","exit":"Sortie","recoverAppSnapshotContent":"Restaure la requête actuelle à la version {version}","searchPlaceholder":"Recherche","allQuery":"Toutes les requêtes","deleteQueryTitle":"Supprimer une requête","unnamed":"Sans nom","publishNewVersion":"Publie une nouvelle version","publishSuccess":"Publié avec succès","version":"Version","desc":"Description"},"snowflake":{"accountIdentifierTooltip":"Voir ","extParamsTooltip":"Configurer des paramètres de connexion supplémentaires"},"lowcoderQuery":{"queryOrgUsers":"Interroger les utilisateurs de l'espace de travail"},"redisQuery":{"rawCommand":"Commande brute","command":"Commande","queryTutorial":"Voir les documents sur les commandes Redis"},"httpQuery":{"bodyFormDataTooltip":"Si {type} est sélectionné, le format de la valeur doit être {objet}. Exemple : {exemple}","text":"Texte","file":"Fichier","extraBodyTooltip":"Les valeurs clés dans le corps supplémentaire seront ajoutées au corps avec des types de données JSON ou Form.","forwardCookies":"Cookies en avant","forwardAllCookies":"Transmettre tous les cookies"},"smtpQuery":{"attachment":"Pièce jointe","attachmentTooltip":"Peut être utilisé avec le composant de téléchargement de fichiers, les données doivent être converties en : ","MIMETypeUrl":"https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types","sender":"Expéditeur","recipient":"Récipiendaire","carbonCopy":"Copie carbone","blindCarbonCopy":"Copie carbone aveugle","subject":"Sujet","content":"Contenu","contentTooltip":"Prend en charge la saisie de texte ou de HTML"},"uiCompCategory":{"dashboards":"Tableaux de bord et rapports","layout":"Mise en page et navigation","forms":"Collecte de données et formulaires","collaboration":"Réunion et collaboration","projectmanagement":"Gestion de projet","scheduling":"Calendrier et programmation","documents":"Gestion des documents et des dossiers","itemHandling":"Traitement des articles et des signatures","multimedia":"Multimédia et animation","integration":"Intégration et extension"},"uiComp":{"autoCompleteCompName":"Auto Complete","autoCompleteCompDesc":"Un champ de saisie qui fournit des suggestions au fur et à mesure que tu tapes, ce qui améliore l'expérience de l'utilisateur et la précision.","autoCompleteCompKeywords":"suggestions, autocomplétion, saisie, entrée","inputCompName":"Entrée","inputCompDesc":"Un champ de saisie de texte de base permettant aux utilisateurs de saisir et de modifier du texte.","inputCompKeywords":"texte, entrée, champ, modifier","textAreaCompName":"Zone de texte","textAreaCompDesc":"Une entrée de texte sur plusieurs lignes pour les contenus plus longs, tels que les commentaires ou les descriptions.","textAreaCompKeywords":"multiligne, textarea, entrée, texte","passwordCompName":"Mot de passe","passwordCompDesc":"Un champ sécurisé pour la saisie du mot de passe, masquant les caractères pour plus de confidentialité.","passwordCompKeywords":"mot de passe, sécurité, entrée, caché","richTextEditorCompName":"Éditeur de texte enrichi","richTextEditorCompDesc":"Un éditeur de texte avancé prenant en charge des options de formatage riches comme le gras, l'italique et les listes.","richTextEditorCompKeywords":"éditeur, texte, formatage, contenu riche","numberInputCompName":"Numéro Entrée","numberInputCompDesc":"Un champ spécifique pour la saisie numérique, avec des commandes pour incrémenter et décrémenter les valeurs.","numberInputCompKeywords":"nombre, entrée, incrémenter, décrémenter","sliderCompName":"Coulisses","sliderCompDesc":"Un composant graphique à curseur pour sélectionner une valeur ou une plage dans une échelle définie.","sliderCompKeywords":"curseur, plage, entrée, graphique","rangeSliderCompName":"Curseur de gamme","rangeSliderCompDesc":"Un curseur à deux poignées pour sélectionner une plage de valeurs, utile pour filtrer ou fixer des limites.","rangeSliderCompKeywords":"gamme, curseur, double poignée, filtre","ratingCompName":"Evaluation","ratingCompDesc":"Un composant permettant de saisir les évaluations des utilisateurs, affichées sous forme d'étoiles.","ratingCompKeywords":"évaluation, étoiles, retour d'information, commentaires","switchCompName":"Interrupteur","switchCompDesc":"Un interrupteur à bascule pour les décisions de type on/off ou oui/non.","switchCompKeywords":"interrupteur, interrupteur à bascule, on/off, contrôle","selectCompName":"Sélectionne","selectCompDesc":"Un menu déroulant permettant de choisir parmi une liste d'options.","selectCompKeywords":"menu déroulant, sélectionner, options, menu","multiSelectCompName":"Multiselect","multiSelectCompDesc":"Un composant qui permet de sélectionner plusieurs éléments dans une liste déroulante.","multiSelectCompKeywords":"multiselect, multiple, dropdown, choices","cascaderCompName":"Cascadeur","cascaderCompDesc":"Une liste déroulante à plusieurs niveaux pour la sélection de données hiérarchiques, comme la sélection d'un lieu.","cascaderCompKeywords":"cascader, hiérarchique, dropdown, niveaux","checkboxCompName":"Case à cocher","checkboxCompDesc":"Une case à cocher standard pour les options qui peuvent être sélectionnées ou désélectionnées.","checkboxCompKeywords":"case à cocher, options, sélectionner, basculer","radioCompName":"Radio","radioCompDesc":"Boutons radio permettant de sélectionner une option parmi un ensemble, lorsqu'un seul choix est autorisé.","radioCompKeywords":"radio, boutons, sélection, choix unique","segmentedControlCompName":"Contrôle segmenté","segmentedControlCompDesc":"Un contrôle avec des options segmentées pour basculer rapidement entre plusieurs choix.","segmentedControlCompKeywords":"segmenté, contrôle, bascule, options","fileUploadCompName":"Téléchargement de fichiers","fileUploadCompDesc":"Un composant pour le téléchargement de fichiers, avec prise en charge du glisser-déposer et de la sélection de fichiers.","fileUploadCompKeywords":"fichier, télécharger, glisser-déposer, sélectionner","dateCompName":"Date","dateCompDesc":"Un composant de sélection de date pour sélectionner des dates dans une interface de calendrier.","dateCompKeywords":"date, choisir, calendrier, sélectionner","dateRangeCompName":"Plage de dates","dateRangeCompDesc":"Un composant permettant de sélectionner une plage de dates, utile pour les systèmes de réservation ou les filtres.","dateRangeCompKeywords":"daterange, sélectionner, réserver, filtrer","timeCompName":"L'heure","timeCompDesc":"Un composant de sélection de l'heure pour choisir des heures spécifiques de la journée.","timeCompKeywords":"heure, choisir, sélectionner, horloge","timeRangeCompName":"Plage de temps","timeRangeCompDesc":"Un composant permettant de sélectionner une plage de temps, souvent utilisé dans les applications de planification.","timeRangeCompKeywords":"timerange, select, scheduling, duration","buttonCompName":"Bouton de formulaire","buttonCompDesc":"Un composant de bouton polyvalent pour soumettre des formulaires, déclencher des actions ou naviguer.","buttonCompKeywords":"bouton, soumettre, action, naviguer","linkCompName":"Lien","linkCompDesc":"Un composant d'affichage d'hyperliens pour la navigation ou la création de liens vers des ressources externes.","linkCompKeywords":"lien, hyperlien, navigation, externe","scannerCompName":"Scanner","scannerCompDesc":"Un composant pour scanner les codes-barres, les codes QR et d'autres données similaires.","scannerCompKeywords":"scanner, code-barres, code QR, scanner","dropdownCompName":"Liste déroulante","dropdownCompDesc":"Un menu déroulant pour afficher de façon compacte une liste d'options.","dropdownCompKeywords":"menu déroulant, menu, options, sélectionner","toggleButtonCompName":"Bouton à bascule","toggleButtonCompDesc":"Un bouton qui peut basculer entre deux états ou options.","toggleButtonCompKeywords":"bascule, bouton, interrupteur, état","textCompName":"Affichage du texte","textCompDesc":"Un composant simple pour afficher un contenu textuel statique ou dynamique incluant la mise en forme Markdown.","textCompKeywords":"texte, affichage, statique, dynamique","tableCompName":"Tableau","tableCompDesc":"Un composant de tableau riche pour afficher des données dans un format de tableau structuré, avec des options de tri et de filtrage, l'affichage de données en arborescence et des rangées extensibles.","tableCompKeywords":"tableau, données, tri, filtrage","imageCompName":"Image","imageCompDesc":"Un composant pour l'affichage d'images, prenant en charge différents formats basés sur des données URI ou Base64.","imageCompKeywords":"image, affichage, média, Base64","progressCompName":"Progrès","progressCompDesc":"Un indicateur visuel de la progression, généralement utilisé pour montrer l'état d'achèvement d'une tâche.","progressCompKeywords":"progrès, indicateur, statut, tâche","progressCircleCompName":"Cercle de progrès","progressCircleCompDesc":"Un indicateur de progrès circulaire, souvent utilisé pour les états de chargement ou les tâches limitées dans le temps.","progressCircleCompKeywords":"cercle, progrès, indicateur, chargement","fileViewerCompName":"Visionneuse de fichiers","fileViewerCompDesc":"Un composant permettant d'afficher divers types de fichiers, notamment des documents et des images.","fileViewerCompKeywords":"fichier, visionneuse, document, image","dividerCompName":"Diviseur","dividerCompDesc":"Un composant de séparation visuelle, utilisé pour séparer le contenu ou les sections dans une mise en page.","dividerCompKeywords":"diviseur, séparateur, mise en page, conception","qrCodeCompName":"Code QR","qrCodeCompDesc":"Un composant permettant d'afficher des codes QR, utiles pour une numérisation rapide et le transfert d'informations.","qrCodeCompKeywords":"QR code, scanner, code-barres, informations","formCompName":"Formulaire","formCompDesc":"Un composant de conteneur pour construire des formulaires structurés avec différents types d'entrée.","formCompKeywords":"formulaire, entrée, conteneur, structure","jsonSchemaFormCompName":"Formulaire de schéma JSON","jsonSchemaFormCompDesc":"Un composant de formulaire dynamique généré sur la base d'un schéma JSON.","jsonSchemaFormCompKeywords":"JSON, schéma, formulaire, dynamique","containerCompName":"Conteneur","containerCompDesc":"Un conteneur à usage général pour la mise en page et l'organisation des éléments de l'interface utilisateur.","containerCompKeywords":"conteneur, mise en page, organisation, interface utilisateur","collapsibleContainerCompName":"Récipient pliable","collapsibleContainerCompDesc":"Un conteneur qui peut être agrandi ou réduit, idéal pour gérer la visibilité du contenu.","collapsibleContainerCompKeywords":"pliable, conteneur, expansion, effondrement","tabbedContainerCompName":"Conteneur à onglets","tabbedContainerCompDesc":"Un conteneur avec navigation par onglets pour organiser le contenu en panneaux distincts.","tabbedContainerCompKeywords":"onglets, conteneur, navigation, panneaux","modalCompName":"Modal","modalCompDesc":"Un composant modal pop-up pour afficher du contenu, des alertes ou des formulaires en focus.","modalCompKeywords":"modal, popup, alerte, formulaire","listViewCompName":"Vue de la liste","listViewCompDesc":"Un composant pour afficher une liste d'éléments ou de données, dans lequel tu peux placer d'autres composants. Comme un répéteur.","listViewCompKeywords":"liste, vue, affichage, répéteur","gridCompName":"Grille","gridCompDesc":"Un composant de grille flexible pour créer des mises en page structurées avec des lignes et des colonnes en tant qu'extension du composant List View.","gridCompKeywords":"grille, disposition, lignes, colonnes","navigationCompName":"Navigation","navigationCompDesc":"Composant de navigation permettant de créer des menus, des fils d'Ariane ou des onglets pour la navigation sur le site.","navigationCompKeywords":"navigation, menu, miettes de pain, onglets","iframeCompName":"IFrame","iframeCompDesc":"Un composant de cadre en ligne pour intégrer des pages web et des apps externes ou du contenu dans l'application.","iframeCompKeywords":"iframe, embed, page web, contenu","customCompName":"Composant personnalisé","customCompDesc":"Un composant flexible et programmable pour créer des éléments d'interface utilisateur uniques, définis par l'utilisateur et adaptés à tes besoins spécifiques.","customCompKeywords":"personnalisé, défini par l'utilisateur, flexible, programmable","moduleCompName":"Module","moduleCompDesc":"Utilise les modules pour créer des micro-applications conçues pour encapsuler des fonctionnalités ou des caractéristiques spécifiques. Les modules peuvent ensuite être intégrés et réutilisés dans toutes les applications.","moduleCompKeywords":"module, micro-app, fonctionnalité, réutilisable","jsonExplorerCompName":"Explorateur JSON","jsonExplorerCompDesc":"Un composant pour explorer visuellement et interagir avec les structures de données JSON.","jsonExplorerCompKeywords":"JSON, explorateur, données, structure","jsonEditorCompName":"Éditeur JSON","jsonEditorCompDesc":"Un composant éditeur pour créer et modifier des données JSON avec validation et coloration syntaxique.","jsonEditorCompKeywords":"JSON, éditeur, modifier, valider","treeCompName":"Arbre","treeCompDesc":"Composant de structure arborescente permettant d'afficher des données hiérarchiques, telles que des systèmes de fichiers ou des organigrammes.","treeCompKeywords":"arbre, hiérarchique, données, structure","treeSelectCompName":"Sélection de l'arbre","treeSelectCompDesc":"Un composant de sélection qui présente les options sous forme d'arbre hiérarchique, permettant des sélections organisées et imbriquées.","treeSelectCompKeywords":"arbre, sélection, hiérarchique, imbriqué","audioCompName":"Audio","audioCompDesc":"Un composant pour intégrer du contenu audio, avec des commandes pour la lecture et le réglage du volume.","audioCompKeywords":"audio, lecture, son, musique","videoCompName":"Vidéo","videoCompDesc":"Un composant multimédia pour l'intégration et la lecture de contenu vidéo, avec prise en charge de divers formats.","videoCompKeywords":"vidéo, multimédia, lecture, intégration","drawerCompName":"Tiroir","drawerCompDesc":"Un composant de panneau coulissant qui peut être utilisé pour une navigation supplémentaire ou l'affichage de contenu, émergeant généralement du bord de l'écran.","drawerCompKeywords":"tiroir, coulissant, panneau, navigation","chartCompName":"Graphique","chartCompDesc":"Un composant polyvalent pour visualiser les données à l'aide de différents types de tableaux et de graphiques.","chartCompKeywords":"diagramme, graphique, données, visualisation","carouselCompName":"Carrousel d'images","carouselCompDesc":"Un composant de carrousel rotatif pour mettre en valeur des images, des bannières ou des diapositives de contenu.","carouselCompKeywords":"carrousel, images, rotation, vitrine","imageEditorCompName":"Éditeur d'images","imageEditorCompDesc":"Un composant interactif pour l'édition et la manipulation d'images, offrant divers outils et filtres.","imageEditorCompKeywords":"image, éditeur, manipuler, outils","mermaidCompName":"Tableau des sirènes","mermaidCompDesc":"Un composant pour rendre les diagrammes complexes et les organigrammes basés sur la syntaxe Mermaid.","mermaidCompKeywords":"sirène, graphiques, diagrammes, organigrammes","calendarCompName":"Calendrier","calendarCompDesc":"Un composant de calendrier pour afficher les dates et les événements, avec des options d'affichage par mois, par semaine ou par jour.","calendarCompKeywords":"calendrier, dates, événements, planification","signatureCompName":"Signature","signatureCompDesc":"Un composant permettant de capturer des signatures numériques, utile pour les processus d'approbation et de vérification.","signatureCompKeywords":"signature, numérique, approbation, vérification","jsonLottieCompName":"Lottie Animation","jsonLottieCompDesc":"Un composant pour afficher les animations Lottie, fournissant des animations légères et évolutives basées sur des données JSON.","jsonLottieCompKeywords":"lottie, animation, JSON, évolutif","timelineCompName":"Chronologie","timelineCompDesc":"Composant permettant d'afficher des événements ou des actions dans un ordre chronologique, représenté visuellement le long d'une ligne de temps linéaire.","timelineCompKeywords":"chronologie, événements, chronologique, histoire","commentCompName":"Commentaire","commentCompDesc":"Un composant permettant d'ajouter et d'afficher des commentaires d'utilisateurs, prenant en charge les réponses par fil de discussion et l'interaction avec l'utilisateur.","commentCompKeywords":"commentaire, discussion, interaction avec l'utilisateur, retour d'information","mentionCompName":"Mention","mentionCompDesc":"Un composant qui prend en charge la mention d'utilisateurs ou de balises dans un contenu textuel, généralement utilisé dans les médias sociaux ou les plateformes collaboratives.","mentionCompKeywords":"mention, tag, utilisateur, médias sociaux","responsiveLayoutCompName":"Mise en page réactive","responsiveLayoutCompDesc":"Composant de mise en page conçu pour s'adapter et répondre aux différentes tailles d'écran et aux différents appareils, ce qui garantit une expérience utilisateur cohérente.","responsiveLayoutCompKeywords":"responsive, layout, adapter, taille d'écran"},"comp":{"menuViewDocs":"Voir la documentation","menuViewPlayground":"Voir l'aire de jeux interactive","menuUpgradeToLatest":"Mise à jour vers la dernière version","nameNotEmpty":"Ne peut pas être vide","nameRegex":"Doit commencer par une lettre et ne contenir que des lettres, des chiffres et des caractères de soulignement (_).","nameJSKeyword":"Ne peut pas être un mot-clé JavaScript","nameGlobalVariable":"Le nom de la variable ne peut pas être global","nameExists":"Le nom {nom} existe déjà","getLatestVersionMetaError":"Le téléchargement de la dernière version a échoué, essaie plus tard.","needNotUpgrade":"La version actuelle est déjà la plus récente.","compNotFoundInLatestVersion":"Composant actuel introuvable dans la dernière version.","upgradeSuccess":"Mise à jour réussie vers la dernière version.","searchProp":"Recherche"},"jsonSchemaForm":{"retry":"Réessayer","resetAfterSubmit":"Réinitialisation après l'envoi du formulaire","jsonSchema":"Schéma JSON","uiSchema":"Schéma de l'interface utilisateur","schemaTooltip":"Voir","defaultData":"Données de formulaire pré-remplies","dataDesc":"Données du formulaire actuel","required":"Exigée","maximum":"La valeur maximale est {valeur}","minimum":"La valeur minimale est {valeur}","exclusiveMaximum":"Doit être inférieur à {valeur}","exclusiveMinimum":"Doit être supérieur à {valeur}","multipleOf":"Doit être un multiple de {valeur}","minLength":"Au moins {valeur} Caractères","maxLength":"Au plus {valeur} Caractères","pattern":"Doit correspondre au modèle {valeur}","format":"Doit correspondre au format {valeur}"},"select":{"inputValueDesc":"Valeur de recherche d'entrée"},"customComp":{"text":"C'est une bonne journée.","triggerQuery":"Requête de déclenchement","updateData":"Mise à jour des données","updateText":"Je suis également de bonne humeur pour développer maintenant mon propre composant personnalisé avec Lowcoder !","sdkGlobalVarName":"Lowcoder","data":"Données que tu veux transmettre au composant personnalisé","code":"Code de ton composant personnalisé"},"tree":{"selectType":"Sélectionne le type","noSelect":"Pas de sélection","singleSelect":"Sélection unique","multiSelect":"Multi Select","checkbox":"Case à cocher","checkedStrategy":"Stratégie vérifiée","showAll":"Tous les nœuds","showParent":"Seulement les nœuds parents","showChild":"Nœuds de l'enfant unique","autoExpandParent":"Auto Expand Parent","checkStrictly":"Vérifier strictement","checkStrictlyTooltip":"Vérifie le nœud de l'arbre avec précision ; le nœud de l'arbre parent et les nœuds de l'arbre enfant ne sont pas associés.","treeData":"Données sur les arbres","treeDataDesc":"Données actuelles sur les arbres","value":"Valeurs par défaut","valueDesc":"Valeurs actuelles","expanded":"Valeurs élargies","expandedDesc":"Valeurs élargies actuelles","defaultExpandAll":"Par défaut Développer tous les nœuds","showLine":"Ligne de spectacle","showLeafIcon":"Montrer l'icône de la feuille","treeDataAsia":"Asie","treeDataChina":"Chine","treeDataBeijing":"Pékin","treeDataShanghai":"Shanghai","treeDataJapan":"Japon","treeDataEurope":"L'Europe","treeDataEngland":"Angleterre","treeDataFrance":"France","treeDataGermany":"Allemagne","treeDataNorthAmerica":"Amérique du Nord","helpLabel":"Étiquette du nœud","helpValue":"Valeur unique du nœud dans l'arbre","helpChildren":"Nœuds des enfants","helpDisabled":"Désactive le nœud","helpSelectable":"Si le nœud est sélectionnable (Type de sélection simple/multi)","helpCheckable":"Afficher ou non la case à cocher (Type de case à cocher)","helpDisableCheckbox":"Désactive la case à cocher (Type de case à cocher)"},"moduleContainer":{"eventTest":"Test de l'événement","methodTest":"Test de la méthode","inputTest":"Test d'entrée"},"password":{"label":"Mot de passe","visibilityToggle":"Afficher la visibilité Bascule"},"richTextEditor":{"toolbar":"Personnaliser la barre d'outils","toolbarDescription":"Tu peux personnaliser la barre d'outils. Pour plus de détails, consulte le site https://quilljs.com/docs/modules/toolbar/.","placeholder":"Saisis tes données, s'il te plaît...","hideToolbar":"Cacher la barre d'outils","content":"Contenu","title":"Titre","save":"Sauvegarde","link":"Lien : ","edit":"Éditer","remove":"Enlever","defaultValue":"Contenu de base"},"numberInput":{"formatter":"Format","precision":"Précision","allowNull":"Autoriser la valeur nulle","thousandsSeparator":"Afficher le séparateur de milliers","controls":"Afficher les boutons d'incrémentation et de décrémentation","step":"Étape","standard":"Standard","percent":"Pourcentage"},"slider":{"step":"Étape","stepTooltip":"La valeur doit être supérieure à 0 et divisible par (Max-Min)"},"rating":{"max":"Valeur nominale max.","allowHalf":"Accorde la moitié des points d'évaluation"},"optionsControl":{"optionList":"Options","option":"Option","optionI":"Option {i}","viewDocs":"Voir les documents","tip":"Les variables \"item\" et \"i\" représentent la valeur et l'index de chaque élément du tableau de données."},"radio":{"options":"Options","horizontal":"Horizontal","horizontalTooltip":"La mise en page horizontale s'enroule sur elle-même lorsqu'elle manque d'espace","vertical":"Vertical","verticalTooltip":"La disposition verticale sera toujours affichée en une seule colonne","autoColumns":"Colonne auto","autoColumnsTooltip":"La disposition en colonnes automatiques réorganise automatiquement l'ordre des articles en fonction de l'espace disponible et s'affiche sous forme de colonnes multiples."},"cascader":{"options":"Données JSON pour afficher les sélections en cascade"},"selectInput":{"valueDesc":"Valeur actuellement sélectionnée","selectedIndexDesc":"L'index de la valeur actuellement sélectionnée, ou -1 si aucune valeur n'est sélectionnée.","selectedLabelDesc":"L'étiquette de la valeur actuellement sélectionnée"},"file":{"typeErrorMsg":"Doit être un nombre avec une unité de taille de fichier valide, ou un nombre d'octets sans unité.","fileEmptyErrorMsg":"Le téléchargement a échoué. La taille du fichier est vide.","fileSizeExceedErrorMsg":"Le téléchargement a échoué. La taille du fichier dépasse la limite.","minSize":"Taille minimale","minSizeTooltip":"La taille minimale des fichiers téléchargés avec des unités de taille de fichier optionnelles (par exemple, \"5kb\", \"10 MB\"). Si aucune unité n'est fournie, la valeur sera considérée comme un nombre d'octets.","maxSize":"Taille maximale","maxSizeTooltip":"La taille maximale des fichiers téléchargés avec des unités de taille de fichier optionnelles (par exemple, \"5kb\", \"10 MB\"). Si aucune unité n'est fournie, la valeur sera considérée comme un nombre d'octets.","single":"Célibataire","multiple":"Multiple","directory":"Répertoire","upload":"Parcourir","fileType":"Types de fichiers","reference":"Réfère-toi à","fileTypeTooltipUrl":"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers","fileTypeTooltip":"Spécification de type de fichier unique","uploadType":"Type de téléchargement","showUploadList":"Afficher la liste des téléchargements","maxFiles":"Fichiers Max","filesValueDesc":"Le contenu du fichier actuellement téléchargé est codé en Base64","filesDesc":"Liste des fichiers actuellement téléchargés. Pour plus de détails, voir","clearValueDesc":"Effacer tous les fichiers","parseFiles":"Analyse les fichiers","parsedValueTooltip1":"Si parseFiles est vrai, les fichiers téléchargés seront analysés en tant qu'objet, tableau ou chaîne. Les données analysées sont accessibles via le tableau parsedValue.","parsedValueTooltip2":"Prend en charge les fichiers Excel, JSON, CSV et texte. Les autres formats renverront un résultat nul."},"date":{"format":"Format","formatTip":"Support : \\N- 'YYYY-MM-DD HH:mm:ss', \\N- 'YYYY-MM-DD', \\N- 'Timestamp' (horodatage)","reference":"Réfère-toi à","showTime":"L'heure du spectacle","start":"Date de début","end":"Date de fin","year":"Année","quarter":"Trimestre","month":"Mois","week":"Semaine","date":"Date","clearAllDesc":"Effacer tout","resetAllDesc":"Réinitialiser tout","placeholder":"Sélectionne la date","placeholderText":"Placeholder","startDate":"Date de début","endDate":"Date de fin"},"time":{"start":"Heure de début","end":"L'heure de la fin","formatTip":"Support : \\HH:mm:ss, Horodatage","format":"Format","placeholder":"Sélectionne l'heure","placeholderText":"Placeholder","startTime":"Heure de début","endTime":"L'heure de la fin"},"button":{"prefixIcon":"Icône de préfixe","suffixIcon":"Icône de suffixe","icon":"Icône","iconSize":"Taille de l'icône","button":"Bouton de formulaire","formToSubmit":"Formulaire à soumettre","default":"Défaut","submit":"Soumettre","textDesc":"Texte actuellement affiché sur le bouton","loadingDesc":"Le bouton est-il en état de chargement ? Si vrai, le bouton actuel est en cours de chargement","formButtonEvent":"Événement"},"link":{"link":"Lien","textDesc":"Texte actuellement affiché sur le lien","loadingDesc":"Le lien est-il en état de chargement ? Si vrai, le lien actuel est en cours de chargement"},"scanner":{"text":"Clique sur Numériser","camera":"Caméra {index}","changeCamera":"Change de caméra","continuous":"Balayage continu","uniqueData":"Ignorer les données en double","maskClosable":"Clique sur le masque pour le fermer","errTip":"Utilise ce composant sous HTTPS ou Localhost"},"dropdown":{"onlyMenu":"Présentoir avec étiquette seulement","textDesc":"Texte actuellement affiché sur le bouton"},"textShow":{"text":"### 👋 Bonjour, {nom}","valueTooltip":"Markdown prend en charge la plupart des balises et attributs HTML. iframe, Script et d'autres balises sont désactivées pour des raisons de sécurité.","verticalAlignment":"Alignement vertical","horizontalAlignment":"Alignement horizontal","textDesc":"Texte affiché dans la zone de texte actuelle"},"table":{"editable":"Modifiable","columnNum":"Colonnes","viewModeResizable":"Largeur de colonne ajustée par l'utilisateur","viewModeResizableTooltip":"Si les utilisateurs peuvent ajuster la largeur des colonnes.","showFilter":"Bouton Afficher le filtre","showRefresh":"Afficher le bouton de rafraîchissement","showDownload":"Afficher le bouton de téléchargement","columnSetting":"Bouton de réglage des colonnes","searchText":"Texte de recherche","searchTextTooltip":"Recherche et filtre les données présentées dans le tableau","showQuickJumper":"Show Quick Jumper","hideOnSinglePage":"Cacher sur une seule page","showSizeChanger":"Bouton de changement de taille","pageSizeOptions":"Options de taille de page","pageSize":"Taille de la page","total":"Nombre total de lignes","totalTooltip":"La valeur par défaut est le nombre d'éléments de données actuels, qui peuvent être obtenus à partir de la requête, par exemple : \\'{{query1.data[0].count}}\\'","filter":"Filtre","filterRule":"Règle de filtrage","chooseColumnName":"Choisis une colonne","chooseCondition":"Choisis une condition","clear":"Clair","columnShows":"Spectacles en colonne","selectAll":"Sélectionner tout","and":"Et","or":"Ou","contains":"Contient","notContain":"Ne contient pas","equals":"Égales","isNotEqual":"N'est pas égal","isEmpty":"Est vide","isNotEmpty":"N'est pas vide","greater":"Plus grand que","greaterThanOrEquals":"Plus grand que ou égal","lessThan":"Moins de","lessThanOrEquals":"Inférieur ou égal","action":"Action","columnValue":"Valeur de la colonne","columnValueTooltip":"\\'{{currentCell}}\\': Données cellulaires actuelles{{currentRow}}\\' : Données de la ligne actuelle{{currentIndex}}\\': Index des données actuelles (à partir de 0)\\NExemple : \\'{{currentCell * 5}}' Afficher 5 fois la valeur d'origine Données.","imageSrc":"Source de l'image","imageSize":"Taille de l'image","columnTitle":"Titre","sortable":"Triable","align":"Alignement","fixedColumn":"Colonne fixe","autoWidth":"Largeur de l'auto","customColumn":"Colonne personnalisée","auto":"Auto","fixed":"Fixe","columnType":"Type de colonne","float":"Flotteur","prefix":"Préfixe","suffix":"Suffixe","text":"Texte","number":"Nombre","link":"Lien","links":"Liens","tag":"Étiquette","date":"Date","dateTime":"Date Heure","badgeStatus":"Statut","button":"Bouton","image":"Image","boolean":"Booléen","rating":"Evaluation","progress":"Progrès","option":"Fonctionnement","optionList":"Liste des opérations","option1":"Opération 1","status":"Statut","statusTooltip":"Valeurs optionnelles : Succès, Erreur, Défaut, Avertissement, Traitement","primaryButton":"Primaire","defaultButton":"Défaut","type":"Type","tableSize":"Taille de la table","hideHeader":"Masquer l'en-tête du tableau","fixedHeader":"En-tête de tableau fixe","fixedHeaderTooltip":"L'en-tête sera fixe pour le tableau à défilement vertical","fixedToolbar":"Barre d'outils fixe","fixedToolbarTooltip":"La barre d'outils sera fixe pour le tableau à défilement vertical en fonction de la position","hideBordered":"Masquer la bordure de la colonne","deleteColumn":"Supprimer une colonne","confirmDeleteColumn":"Confirme la suppression de la colonne : ","small":"S","middle":"M","large":"L","refreshButtonTooltip":"Les données actuelles changent, clique pour régénérer la colonne.","changeSetDesc":"Un objet représentant les modifications apportées à un tableau modifiable ne contient que la cellule modifiée. Les lignes passent en premier et les colonnes en second.","selectedRowDesc":"Fournit des données sur la ligne actuellement sélectionnée, indiquant la ligne qui déclenche un événement de clic si l'utilisateur clique sur un bouton/lien de la ligne.","selectedRowsDesc":"Utile en mode de sélection multiple, comme SelectedRow","pageNoDesc":"Page d'affichage actuelle, à partir de 1","pageSizeDesc":"Combien de lignes par page ?","sortColumnDesc":"Le nom de la colonne triée actuellement sélectionnée","sortDesc":"Si la ligne actuelle est en ordre décroissant","pageOffsetDesc":"Le début actuel de la pagination, utilisé pour la pagination afin d'obtenir des données. Exemple : Select * from Users Limit \\'{{table1.pageSize}}\\N- Décalage \\N- Décalage \\N- Décalage \\N- Décalage{{table1.pageOffset}}\\'","displayDataDesc":"Données affichées dans le tableau actuel","selectedIndexDesc":"Index sélectionné dans les données d'affichage","filterDesc":"Paramètres de filtrage des tables","dataDesc":"Les données JSON du tableau","saveChanges":"Sauvegarder les changements","cancelChanges":"Annuler les changements","rowSelectChange":"Changement de sélection de ligne","rowClick":"Cliquez sur la rangée","rowExpand":"Extension de la rangée","filterChange":"Changement de filtre","sortChange":"Changement de tri","pageChange":"Changement de page","refresh":"Rafraîchir","rowColor":"Couleur de ligne conditionnelle","rowColorDesc":"Définit conditionnellement la couleur de la ligne en fonction des variables facultatives : CurrentRow, CurrentOriginalIndex, CurrentIndex, ColumnTitle. Par exemple : \\'{{ currentRow.id > 3 ? %r@\\\"green%r@\\\" : %r@\\\"red%r@\\\" }}\\'","cellColor":"Couleur conditionnelle des cellules","cellColorDesc":"Définir conditionnellement la couleur de la cellule en fonction de la valeur de la cellule à l'aide de CurrentCell. Par exemple : \\N{ currentCell == 3 ? %r@\\\"green%r@\\\" : %r@\\\"red%r@\\\" }}\\'","saveChangesNotBind":"Aucun gestionnaire d'événements n'est configuré pour enregistrer les modifications. Lier au moins un gestionnaire d'événements avant de cliquer.","dynamicColumn":"Utiliser le réglage dynamique des colonnes","dynamicColumnConfig":"Réglage de la colonne","dynamicColumnConfigDesc":"Paramètres dynamiques des colonnes. Accepte un tableau de noms de colonnes. Toutes les colonnes sont visibles par défaut. Exemple : [%r@\\\"id%r@\\\", %r@\\\"name%r@\\\"]","position":"Position","showDataLoadSpinner":"Montrer le rouleau pendant le chargement des données","showValue":"Montrer la valeur","expandable":"Extensible","configExpandedView":"Configurer la vue étendue","toUpdateRowsDesc":"Un tableau d'objets pour les lignes à mettre à jour dans les tableaux modifiables.","empty":"Vide","falseValues":"Texte si faux","allColumn":"Tous","visibleColumn":"Visible","emptyColumns":"Aucune colonne n'est actuellement visible"},"image":{"src":"Source de l'image","srcDesc":"La source de l'image. Il peut s'agir d'une URL, d'un chemin ou d'une chaîne Base64. par exemple : data:image/png;base64, AAA... CCC","supportPreview":"Support Cliquez sur l'aperçu (zoom)","supportPreviewTip":"Efficace lorsque la source de l'image est valide"},"progress":{"value":"Valeur","valueTooltip":"Le pourcentage d'achèvement est une valeur comprise entre 0 et 100.","showInfo":"Montrer la valeur","valueDesc":"Valeur de la progression actuelle, comprise entre 0 et 100","showInfoDesc":"Afficher ou non la valeur actuelle de la progression"},"fileViewer":{"invalidURL":"Saisis une URL valide ou une chaîne de caractères Base64","src":"URI de fichier","srcTooltip":"Prévisualise le contenu des liens fournis en y intégrant du HTML, les données encodées en Base64 peuvent également être prises en charge, par exemple : data:application/pdf ; base64,AAA... CCC","srcDesc":"L'URI du fichier"},"divider":{"title":"Titre","align":"Alignement","dashed":"En pointillé","dashedDesc":"Utiliser ou non la ligne pointillée","titleDesc":"Titre du diviseur","alignDesc":"Alignement du titre de l'intercalaire"},"QRCode":{"value":"Valeur du contenu du code QR","valueTooltip":"La valeur contient un maximum de 2953 caractères. La Valeur du code QR peut encoder différents types de données, notamment des messages texte, des URL, des coordonnées (VCard/meCard), des identifiants de connexion Wi-Fi, des adresses e-mail, des numéros de téléphone, des SMS, des coordonnées de géolocalisation, des détails d'événements du calendrier, des informations de paiement, des adresses de crypto-monnaies et des liens de téléchargement d'applis","valueDesc":"La valeur du contenu du code QR","level":"Niveau de tolérance aux fautes","levelTooltip":"Se réfère à la capacité du code QR à être scanné même si une partie est bloquée. Plus le niveau est élevé, plus le code est complexe.","includeMargin":"Afficher la marge","image":"Affiche l'image au centre","L":"L (faible)","M":"M (Moyen)","Q":"Q (Quartile)","H":"H (Haut)","maxLength":"Le contenu est trop long. Règle la longueur à moins de 2953 caractères."},"jsonExplorer":{"indent":"Indentation de chaque niveau","expandToggle":"Développer l'arbre JSON","theme":"Thème de couleur","valueDesc":"Données JSON actuelles","default":"Défaut","defaultDark":"Foncé par défaut","neutralLight":"Lumière neutre","neutralDark":"Neutre foncé","azure":"L'azur","darkBlue":"Bleu foncé"},"audio":{"src":"Source audio URI ou chaîne Base64","defaultSrcUrl":"https://cdn.pixabay.com/audio/2023/07/06/audio_e12e5bea9d.mp3","autoPlay":"Jeu automatique","loop":"Boucle","srcDesc":"URI audio actuel ou chaîne Base64 comme data:audio/mpeg;base64,AAA... CCC","play":"Jouer","playDesc":"Déclenché lors de la lecture d'un fichier audio","pause":"Pause","pauseDesc":"Déclenché lorsque l'audio est en pause","ended":"Terminé","endedDesc":"Déclenché à la fin de la lecture de l'audio"},"video":{"src":"URI de la source vidéo ou chaîne Base64","defaultSrcUrl":"https://www.youtube.com/watch?v=pRpeEdMmmQ0","poster":"URL de l'affiche","defaultPosterUrl":"","autoPlay":"Jeu automatique","loop":"Boucle","controls":"Cacher les contrôles","volume":"Volume","playbackRate":"Taux de lecture","posterTooltip":"La valeur par défaut est la première image de la vidéo.","autoPlayTooltip":"Une fois que la vidéo est chargée, elle est lue automatiquement. Si tu changes cette valeur de True à False, la vidéo sera mise en pause. (Si un poster est défini, il sera lu par le bouton Poster)","controlsTooltip":"Cache les contrôles de lecture vidéo. Peut ne pas être entièrement pris en charge par toutes les sources vidéo.","volumeTooltip":"Règle le volume du lecteur, entre 0 et 1","playbackRateTooltip":"Règle le taux du joueur, entre 1 et 2","srcDesc":"URI audio actuel ou chaîne Base64 comme data:video/mp4;base64, AAA... CCC","play":"Jouer","playDesc":"Déclenché lors de la lecture de la vidéo","pause":"Pause","pauseDesc":"Déclenché lorsque la vidéo est mise en pause","load":"Charge","loadDesc":"Déclenché lorsque le chargement de la ressource vidéo est terminé","ended":"Terminé","endedDesc":"Déclenché à la fin de la lecture de la vidéo","currentTimeStamp":"La position de lecture actuelle de la vidéo en secondes","duration":"Durée totale de la vidéo en secondes"},"media":{"playDesc":"Commence la lecture du média.","pauseDesc":"Met en pause la lecture du média.","loadDesc":"Réinitialise le média au début et redémarre Sélectionne la ressource média.","seekTo":"Cherche jusqu'au nombre de secondes donné, ou une fraction si la quantité est comprise entre 0 et 1","seekToAmount":"Nombre de secondes, ou fraction s'il est compris entre 0 et 1","showPreview":"Avant-première du spectacle"},"rangeSlider":{"start":"Valeur de départ","end":"Valeur finale","step":"Taille de l'étape","stepTooltip":"Granularité du curseur, la valeur doit être supérieure à 0 et divisible par (Max-Min)."},"iconControl":{"selectIcon":"Sélectionne une icône","insertIcon":"Insérer une icône","insertImage":"Insérer une image ou "},"millisecondsControl":{"timeoutTypeError":"Saisis la période de temporisation correcte en ms, l'entrée actuelle est : {valeur}","timeoutLessThanMinError":"L'entrée doit être supérieure à {left}, l'entrée actuelle est : {valeur}"},"selectionControl":{"single":"Célibataire","multiple":"Multiple","close":"Fermer","mode":"Sélectionne le mode"},"container":{"title":"Titre du conteneur affiché"},"drawer":{"placement":"Placement des tiroirs","size":"Taille","top":"Haut","right":"Droit","bottom":"Le fond","left":"Gauche","widthTooltip":"Pixel ou pourcentage, par exemple 520, 60%","heightTooltip":"Pixel, par exemple 378","openDrawerDesc":"Tiroir ouvert","closeDrawerDesc":"Fermer le tiroir","width":"Largeur du tiroir","height":"Hauteur du tiroir"},"meeting":{"logLevel":"Niveau du journal du SDK Agora","placement":"Placement des tiroirs de réunion","meeting":"Paramètres de la réunion","cameraView":"Vue de la caméra","cameraViewDesc":"Vue de la caméra de l'utilisateur local (hôte)","screenShared":"Écran partagé","screenSharedDesc":"Écran partagé par l'utilisateur local (hôte)","audioUnmuted":"Audio Unmuted","audioMuted":"Audio Muted","videoClicked":"Vidéo cliquée","videoOff":"Video Off","videoOn":"Vidéo sur","size":"Taille","top":"Haut","host":"Hôte de la salle de réunion. Tu dois gérer l'hôte comme une application logique propre.","participants":"Participants de la salle de réunion","shareScreen":"Écran d'affichage partagé par l'utilisateur local","appid":"ID de l'application Agora","meetingName":"Nom de la réunion","localUserID":"ID de l'utilisateur de l'hôte","userName":"Nom d'utilisateur de l'hôte","rtmToken":"Token RTM Agora","rtcToken":"Jeton RTC Agora","noVideo":"Pas de vidéo","profileImageUrl":"URL de l'image du profil","right":"Droit","bottom":"Le fond","videoId":"ID du flux vidéo","audioStatus":"État de l'audio","left":"Gauche","widthTooltip":"Pixel ou pourcentage, par exemple 520, 60%","heightTooltip":"Pixel, par exemple 378","openDrawerDesc":"Tiroir ouvert","closeDrawerDesc":"Fermer le tiroir","width":"Largeur du tiroir","height":"Hauteur du tiroir","actionBtnDesc":"Bouton d'action","broadCast":"Messages diffusés","title":"Titre de la réunion","meetingCompName":"Agora Meeting Controller","sharingCompName":"Partage d'écran Flux","videoCompName":"Flux de caméra","videoSharingCompName":"Partage d'écran Flux","meetingControlCompName":"Bouton de commande","meetingCompDesc":"Composante de la réunion","meetingCompControls":"Contrôle des réunions","meetingCompKeywords":"Agora Meeting, Web Meeting, Collaboration","iconSize":"Taille de l'icône","userId":"ID de l'utilisateur de l'hôte","roomId":"ID de la pièce","meetingActive":"Réunion en cours","messages":"Messages diffusés"},"settings":{"title":"Réglages","userGroups":"Groupes d'utilisateurs","organization":"Espaces de travail","audit":"Journaux d'audit","theme":"Thèmes","plugin":"Plugins","advanced":"Avancé","lab":"Laboratoire","branding":"L'image de marque","oauthProviders":"Fournisseurs OAuth","appUsage":"Journal d'utilisation de l'application","environments":"Environnements","premium":"Premium"},"memberSettings":{"admin":"Admin","adminGroupRoleInfo":"L'administrateur peut gérer les membres et les ressources du groupe","adminOrgRoleInfo":"Les administrateurs possèdent toutes les ressources et peuvent gérer les groupes.","member":"Membre","memberGroupRoleInfo":"Le membre peut voir les membres du groupe","memberOrgRoleInfo":"Les membres ne peuvent utiliser ou visiter que les ressources auxquelles ils ont accès.","title":"Les membres","createGroup":"Créer un groupe","newGroupPrefix":"Nouveau groupe ","allMembers":"Tous les membres","deleteModalTitle":"Supprimer ce groupe","deleteModalContent":"Le groupe supprimé ne peut pas être restauré. Es-tu sûr de vouloir supprimer le groupe ?","addMember":"Ajouter des membres","nameColumn":"Nom de l'utilisateur","joinTimeColumn":"Temps d'adhésion","actionColumn":"Fonctionnement","roleColumn":"Rôle","exitGroup":"Groupe de sortie","moveOutGroup":"Retirer du groupe","inviteUser":"Invite les membres","exitOrg":"Pars","exitOrgDesc":"Es-tu sûr de vouloir quitter cet espace de travail.","moveOutOrg":"Enlever","moveOutOrgDescSaasMode":"Es-tu sûr de vouloir supprimer l'utilisateur {nom} de cet espace de travail ?","moveOutOrgDesc":"Es-tu sûr de vouloir supprimer l'utilisateur {nom} ? Cette action ne peut pas être récupérée.","devGroupTip":"Les membres du groupe de développeurs ont des privilèges pour créer des applications et des sources de données.","lastAdminQuit":"Le dernier administrateur ne peut pas quitter.","organizationNotExist":"L'espace de travail actuel n'existe pas","inviteUserHelp":"Tu peux copier le lien d'invitation pour l'envoyer à l'utilisateur","inviteUserLabel":"Lien d'invitation :","inviteCopyLink":"Copier le lien","inviteText":"{nom d'utilisateur} t'invite à rejoindre l'espace de travail %r@\\\"{organisation}%r@\\\", clique sur le lien pour le rejoindre : {inviteLink}","groupName":"Nom du groupe","createTime":"Créer du temps","manageBtn":"Gérer","userDetail":"Détail","syncDeleteTip":"Ce groupe a été supprimé du carnet d'adresses Source","syncGroupTip":"Ce groupe est un groupe de synchronisation du carnet d'adresses et ne peut pas être modifié."},"orgSettings":{"newOrg":"Nouvel espace de travail (Organisation)","title":"Espace de travail","createOrg":"Créer un espace de travail (organisation)","deleteModalTitle":"Es-tu sûr de vouloir supprimer cet espace de travail ?","deleteModalContent":"Tu es sur le point de supprimer cet espace de travail {permanentlyDelete}. Une fois supprimé, l'espace de travail {notRestored}.","permanentlyDelete":"De façon permanente","notRestored":"Ne peut pas être restauré","deleteModalLabel":"Saisis le nom de l'espace de travail {nom} pour confirmer l'opération :","deleteModalTip":"Saisis le nom de l'espace de travail","deleteModalErr":"Le nom de l'espace de travail est incorrect","deleteModalBtn":"Supprimer","editOrgTitle":"Modifier les informations sur l'espace de travail","orgNameLabel":"Nom de l'espace de travail :","orgNameCheckMsg":"Le nom de l'espace de travail ne peut pas être vide","orgLogo":"Logo de l'espace de travail :","logoModify":"Modifier l'image","inviteSuccessMessage":"Réussir à rejoindre l'espace de travail","inviteFailMessage":"Échec de la jonction avec l'espace de travail","uploadErrorMessage":"Erreur de téléchargement","orgName":"Nom de l'espace de travail"},"freeLimit":"Essai gratuit","tabbedContainer":{"switchTab":"Onglet \"Switch\" (interrupteur)","switchTabDesc":"Déclenché lors du passage d'un onglet à l'autre","tab":"Onglets","atLeastOneTabError":"Le conteneur d'onglets conserve au moins un onglet.","selectedTabKeyDesc":"Onglet actuellement sélectionné","iconPosition":"Position de l'icône"},"formComp":{"containerPlaceholder":"Fais glisser les composants depuis le panneau de droite ou","openDialogButton":"Génère un formulaire à partir d'une de tes sources de données","resetAfterSubmit":"Réinitialisation après une soumission réussie","initialData":"Données initiales","disableSubmit":"Désactiver la soumission","success":"Formulaire généré avec succès","selectCompType":"Sélectionne le type de composant","dataSource":"Source des données : ","selectSource":"Sélectionne la source","table":"Tableau : ","selectTable":"Sélectionne une table","columnName":"Nom de la colonne","dataType":"Type de données","compType":"Type de composant","required":"Exigée","generateForm":"Générer un formulaire","compSelectionError":"Type de colonne non configuré","compTypeNameError":"Impossible d'obtenir le nom du type de composant","noDataSourceSelected":"Aucune source de données sélectionnée","noTableSelected":"Aucune table sélectionnée","noColumn":"Pas de colonne","noColumnSelected":"Aucune colonne sélectionnée","noDataSourceFound":"Aucune source de données prise en charge n'a été trouvée. Créer une nouvelle source de données","noTableFound":"Aucun tableau n'a été trouvé dans cette source de données, choisis une autre source de données.","noColumnFound":"Aucune colonne prise en charge n'a été trouvée dans ce tableau. Choisis un autre tableau","formTitle":"Titre du formulaire","name":"Nom","nameTooltip":"Le nom de l'attribut dans les données du formulaire, lorsqu'il est laissé en blanc, prend par défaut le nom du composant.","notSupportMethod":"Non pris en charge Méthodes : ","notValidForm":"Le formulaire n'est pas valide","resetDesc":"Réinitialiser les données du formulaire à la valeur par défaut","clearDesc":"Effacer les données du formulaire","setDataDesc":"Définir les données du formulaire","valuesLengthError":"Paramètre Numéro Erreur","valueTypeError":"Type de paramètre Erreur","dataDesc":"Données du formulaire actuel","loadingDesc":"Le formulaire est-il en train de se charger ?"},"modalComp":{"close":"Fermer","closeDesc":"Déclenché lorsque la boîte de dialogue modale est fermée","openModalDesc":"Ouvre la boîte de dialogue","closeModalDesc":"Ferme la boîte de dialogue","visibleDesc":"Est-il visible ? Si c'est le cas, la boîte de dialogue actuelle s'affichera.","modalHeight":"Hauteur modale","modalHeightTooltip":"Pixel, Exemple : 222","modalWidth":"Largeur modale","modalWidthTooltip":"Nombre ou pourcentage, exemple : 520, 60%"},"listView":{"noOfRows":"Nombre de rangs","noOfRowsTooltip":"Nombre de lignes dans la liste - Généralement défini par une variable (par exemple, \"\\\"){{query1.data.length}}\\') pour présenter les résultats de la requête","noOfColumns":"Nombre de colonnes","itemIndexName":"Nom de l'index de l'élément de données","itemIndexNameDesc":"Nom de la variable se référant à l'index de l'article, par défaut {default}","itemDataName":"Nom de l'objet de l'élément de données","itemDataNameDesc":"Nom de la variable faisant référence à l'objet de données de l'élément, par défaut {default}","itemsDesc":"Exposer les données des composants dans une liste","dataDesc":"Les données JSON utilisées dans la liste actuelle","dataTooltip":"Si tu ne définis qu'un nombre, ce champ sera considéré comme le nombre de lignes et les données seront considérées comme vides."},"navigation":{"addText":"Ajouter un élément de sous-menu","logoURL":"Navigation Logo URL","horizontalAlignment":"Alignement horizontal","logoURLDesc":"Tu peux afficher un logo sur le côté gauche en entrant une valeur URI ou une chaîne Base64 comme data:image/png;base64,AAA... CCC","itemsDesc":"Éléments du menu de navigation hiérarchique"},"droppadbleMenuItem":{"subMenu":"Sous-menu {numéro}"},"navItemComp":{"active":"Actif"},"iframe":{"URLDesc":"L'URL source du contenu de l'IFrame. Assure-toi que l'URL est HTTPS ou localhost. Assure-toi également que l'URL n'est pas bloquée par la politique de sécurité du contenu (CSP) du navigateur. L'en-tête \"X-Frame-Options\" ne doit pas avoir pour valeur \"DENY\" ou \"SAMEORIGIN\".","allowDownload":"Autoriser les téléchargements","allowSubmitForm":"Autoriser le formulaire de soumission","allowMicrophone":"Autoriser le microphone","allowCamera":"Autoriser l'appareil photo","allowPopup":"Autoriser les fenêtres contextuelles"},"switchComp":{"defaultValue":"Valeur booléenne par défaut","open":"Sur","close":"Off","openDesc":"Déclenché lorsque l'interrupteur est mis en marche","closeDesc":"Déclenché lorsque l'interrupteur est éteint","valueDesc":"État actuel du commutateur"},"signature":{"tips":"Texte de l'indice","signHere":"Signe ici","showUndo":"Afficher l'annulation","showClear":"Afficher l'effacement"},"localStorageComp":{"valueDesc":"Tous les éléments de données actuellement stockés","setItemDesc":"Ajouter un article","removeItemDesc":"Retirer un article","clearItemDesc":"Effacer tous les articles"},"utilsComp":{"openUrl":"Ouvrir l'URL","openApp":"Ouvrir l'application","copyToClipboard":"Copier dans le presse-papiers","downloadFile":"Télécharger le fichier"},"messageComp":{"info":"Envoyer une notification","success":"Envoyer une notification de réussite","warn":"Envoyer une notification d'avertissement","error":"Envoyer une notification d'erreur"},"themeComp":{"switchTo":"Thème de l'interrupteur"},"transformer":{"preview":"Aperçu","docLink":"En savoir plus sur Transformers...","previewSuccess":"Aperçu du succès","previewFail":"Échec de la prévisualisation","deleteMessage":"Effacer le succès du transformateur. Tu peux utiliser {undoKey} pour annuler.","documentationText":"Les transformateurs sont conçus pour la transformation des données et la réutilisation de ton code JavaScript à plusieurs lignes. Utilise les transformateurs pour adapter les données des requêtes ou des composants aux besoins de ton application locale. Contrairement aux requêtes JavaScript, les transformateurs sont conçus pour effectuer des opérations en lecture seule, ce qui signifie que tu ne peux pas déclencher une requête ou mettre à jour un état temporaire à l'intérieur d'un transformateur."},"temporaryState":{"value":"Valeur d'initialisation","valueTooltip":"La valeur initiale stockée dans l'état temporaire peut être n'importe quelle valeur JSON valide.","docLink":"En savoir plus sur les États temporaires...","pathTypeError":"Le chemin doit être soit une chaîne, soit un tableau de valeurs","unStructuredError":"Les données non structurées {prev} ne peuvent pas être mises à jour par {chemin}","valueDesc":"Valeur de l'état temporaire","deleteMessage":"L'état temporaire est supprimé avec succès. Tu peux utiliser {undoKey} pour annuler.","documentationText":"Les états temporaires dans Lowcoder sont une fonctionnalité puissante utilisée pour gérer des variables complexes qui mettent à jour dynamiquement l'état des composants de ton application. Ces états servent de stockage intermédiaire ou transitoire pour les données qui peuvent changer au fil du temps en raison des interactions de l'utilisateur ou d'autres processus."},"dataResponder":{"data":"Données","dataDesc":"Données du répondant actuel","dataTooltip":"Lorsque ces données sont modifiées, elles déclenchent des actions ultérieures.","docLink":"En savoir plus sur les Data Responders...","deleteMessage":"Le répondeur de données est supprimé avec succès. Tu peux utiliser {undoKey} pour annuler.","documentationText":"Lorsque tu développes une application, tu peux assigner des événements aux composants pour surveiller les modifications apportées à des données spécifiques. Par exemple, un composant Table peut avoir des événements tels que %r@\\\"Row select change%r@\\\", %r@\\\"Filter change%r@\\\", %r@\\\"Sort change%r@\\\", et %r@\\\"Page change%r@\\\" pour suivre les modifications de la propriété selectedRow. Cependant, pour les changements dans les états temporaires, les transformateurs ou les résultats des requêtes, lorsque les événements standard ne sont pas disponibles, les répondeurs de données sont utilisés. Ils te permettent de détecter et de réagir à toute modification des données."},"theme":{"title":"Thèmes","createTheme":"Créer un thème","themeName":"Nom du thème :","themeNamePlaceholder":"Saisis un nom de thème","defaultThemeTip":"Thème par défaut :","createdThemeTip":"Le thème que tu as créé :","option":"Option{index}","input":"Entrée","confirm":"Ok","emptyTheme":"Aucun thème disponible","click":"","toCreate":"","nameColumn":"Nom","defaultTip":"Défaut","updateTimeColumn":"Heure de mise à jour","edit":"Éditer","cancelDefaultTheme":"Thème par défaut non défini","setDefaultTheme":"Définir comme thème par défaut","copyTheme":"Thème dupliqué","setSuccessMsg":"Réglage réussi","cancelSuccessMsg":"Unsetting Succeeded","deleteSuccessMsg":"Suppression réussie","checkDuplicateNames":"Le nom du thème existe déjà, saisis-le à nouveau.","copySuffix":" Copie","saveSuccessMsg":"Sauvegardé avec succès","leaveTipTitle":"Conseils","leaveTipContent":"Tu n'es pas encore sauvée, confirme ton départ ?","leaveTipOkText":"Pars","goList":"Retour à la liste","saveBtn":"Sauvegarde","mainColor":"Couleurs principales","text":"Couleurs du texte","defaultTheme":"Défaut","yellow":"Jaune","green":"Vert","previewTitle":"Aperçu du thème Exemple de composants qui utilisent les couleurs de ton thème","dateColumn":"Date","emailColumn":"Courriel","phoneColumn":"Téléphone","subTitle":"Titre","linkLabel":"Lien","linkUrl":"app.lowcoder.cloud","progressLabel":"Progrès","sliderLabel":"Coulisses","radioLabel":"Radio","checkboxLabel":"Case à cocher","buttonLabel":"Bouton de formulaire","switch":"Interrupteur","previewDate":"16/10/2022","previewEmail1":"ted.com","previewEmail2":"skype.com","previewEmail3":"imgur.com","previewEmail4":"globo.com","previewPhone1":"+63-317-333-0093","previewPhone2":"+30-668-580-6521","previewPhone3":"+86-369-925-2071","previewPhone4":"+7-883-227-8093","chartPreviewTitle":"Aperçu du style de graphique","chartSpending":"Dépenses","chartBudget":"Budget","chartAdmin":"Administration","chartFinance":"Finances","chartSales":"Vente","chartFunnel":"Diagramme en entonnoir","chartShow":"Montrer","chartClick":"Clique sur","chartVisit":"Visiter","chartQuery":"Demande","chartBuy":"Acheter"},"pluginSetting":{"title":"Plugins","npmPluginTitle":"npm Plugins","npmPluginDesc":"Configure les plugins npm pour toutes les applications de l'espace de travail actuel.","npmPluginEmpty":"Aucun plugin npm n'a été ajouté.","npmPluginAddButton":"Ajouter un plugin npm","saveSuccess":"Sauvegardé avec succès"},"advanced":{"title":"Avancé","defaultHomeTitle":"Page d'accueil par défaut","defaultHomeHelp":"La page d'accueil est l'application que tous les non-développeurs verront par défaut lorsqu'ils se connecteront. Note : Assure-toi que l'application sélectionnée est accessible aux non-développeurs.","defaultHomePlaceholder":"Sélectionne la page d'accueil par défaut","saveBtn":"Sauvegarde","preloadJSTitle":"Précharger JavaScript","preloadJSHelp":"Configure le code JavaScript préchargé pour toutes les applications de l'espace de travail actuel.","preloadCSSTitle":"Précharger le CSS","preloadCSSHelp":"Configure le code CSS préchargé pour toutes les applications de l'espace de travail actuel.","preloadCSSApply":"Appliquer à la page d'accueil de l'espace de travail","preloadLibsTitle":"Bibliothèque JavaScript","preloadLibsHelp":"Les bibliothèques JavaScript sont préchargées pour toutes les applications de l'espace de travail actuel, et le système intègre lodash, day.js, uuid, numbro pour une utilisation directe. Les bibliothèques JavaScript sont chargées avant l'initialisation de l'application, ce qui a un certain impact sur les performances de l'application.","preloadLibsEmpty":"Aucune bibliothèque JavaScript n'a été ajoutée","preloadLibsAddBtn":"Ajouter une bibliothèque","saveSuccess":"Sauvegardé avec succès","AuthOrgTitle":"Écran de bienvenue de l'espace de travail","AuthOrgDescrition":"L'URL permettant à tes utilisateurs de se connecter à l'espace de travail actuel."},"branding":{"title":"L'image de marque","logoTitle":"Logo","logoHelp":".JPG, .SVG ou .PNG uniquement","faviconTitle":"Favicon","faviconHelp":".JPG, .SVG ou .PNG uniquement","brandNameTitle":"Nom de la marque","headColorTitle":"Couleur de la tête","save":"Sauvegarde","saveSuccessMsg":"Sauvegardé avec succès","upload":"Cliquer pour télécharger"},"networkMessage":{"0":"Échec de la connexion au serveur, vérifie ton réseau","401":"Échec de l'authentification, reconnecte-toi","403":"Pas de permission, contacte l'administrateur pour obtenir une autorisation.","500":"Service occupé, merci de réessayer plus tard","timeout":"Délai de requête"},"share":{"title":"Partager","viewer":"Visionneuse","editor":"Éditeur","owner":"Propriétaire","datasourceViewer":"Peut utiliser","datasourceOwner":"Peut gérer"},"debug":{"title":"Titre","switch":"Composant du commutateur : "},"module":{"emptyText":"Pas de données","circularReference":"Référence circulaire, le module/l'application en cours ne peut pas être utilisé(e) !","emptyTestInput":"Le module actuel n'a pas d'entrée à tester","emptyTestMethod":"Le module actuel n'a pas de méthode pour tester","name":"Nom","input":"Entrée","params":"Params","emptyParams":"Aucun paramètre n'a été ajouté","emptyInput":"Aucune entrée n'a été ajoutée","emptyMethod":"Aucune méthode n'a été ajoutée","emptyOutput":"Aucune sortie n'a été ajoutée","data":"Données","string":"Chaîne","number":"Nombre","array":"Array","boolean":"Booléen","query":"Demande","autoScaleCompHeight":"Balances à hauteur de composant avec conteneur","excuteMethod":"Exécuter la méthode {nom}","method":"Méthode","action":"Action","output":"Sortie","nameExists":"Nom {nom} existe déjà","eventTriggered":"L'événement {nom} est déclenché","globalPromptWhenEventTriggered":"Affiche une invite globale lorsqu'un événement est déclenché","emptyEventTest":"Le module actuel n'a aucun événement à tester","emptyEvent":"Aucun événement n'a été ajouté","event":"Événement"},"resultPanel":{"returnFunction":"La valeur de retour est une fonction.","consume":"{heure}","JSON":"Montrer JSON"},"createAppButton":{"creating":"Créer...","created":"Créer {nom}"},"apiMessage":{"authenticationFail":"L'authentification de l'utilisateur a échoué, tu dois te reconnecter.","verifyAccount":"Besoin de vérifier le compte","functionNotSupported":"La version actuelle ne prend pas en charge cette fonction. Contacte l'équipe commerciale de Lowcoder pour mettre à jour ton compte."},"globalErrorMessage":{"createCompFail":"Créer le composant {comp} Échec","notHandledError":"{méthode} Méthode non exécutée"},"aggregation":{"navLayout":"Barre de navigation","chooseApp":"Choisis l'application","iconTooltip":"Prend en charge le lien src de l'image ou la chaîne Base64 comme data:image/png;base64,AAA... CCC","hideWhenNoPermission":"Caché pour les utilisateurs non autorisés","queryParam":"Paramètres de la requête URL","hashParam":"URL Hash Params","tabBar":"Barre d'onglets","emptyTabTooltip":"Configure cette page dans le volet de droite"},"appSetting":{"450":"450px (Téléphone)","800":"800px (Tablette)","1440":"1440px (ordinateur portable)","1920":"1920px (écran large)","3200":"3200px (Super grand écran)","title":"Paramètres généraux de l'application","autofill":"Remplissage automatique","userDefined":"Sur mesure","default":"Défaut","tooltip":"Ferme la fenêtre contextuelle après le réglage","canvasMaxWidth":"Largeur maximale du canevas pour cette application","userDefinedMaxWidth":"Largeur maximale personnalisée","inputUserDefinedPxValue":"Saisis une valeur de pixel personnalisée","maxWidthTip":"La largeur maximale doit être supérieure ou égale à 350","themeSetting":"Thème de style appliqué","themeSettingDefault":"Défaut","themeCreate":"Créer un thème"},"customShortcut":{"title":"Raccourcis personnalisés","shortcut":"Raccourci","action":"Action","empty":"Pas de raccourcis","placeholder":"Appuyer sur Raccourci","otherPlatform":"Autre","space":"L'espace"},"profile":{"orgSettings":"Paramètres de l'espace de travail","switchOrg":"Change d'espace de travail","joinedOrg":"Mes espaces de travail","createOrg":"Créer un espace de travail","logout":"Déconnecte-toi","personalInfo":"Mon profil","bindingSuccess":"Liaison {nom de la source} Succès","uploadError":"Erreur de téléchargement","editProfilePicture":"Modifier","nameCheck":"Le nom ne peut pas être vide","name":"Nom : ","namePlaceholder":"Saisis ton nom","toBind":"Pour relier","binding":"Est contraignant","bindError":"Erreur de paramètre, actuellement non pris en charge Liaison.","bindName":"Lier {nom}","loginAfterBind":"Après la liaison, tu peux utiliser {nom} pour te connecter","bindEmail":"Lier l'email :","email":"Courriel","emailCheck":"Saisis un courriel valide","emailPlaceholder":"Saisis ton courriel","submit":"Soumettre","bindEmailSuccess":"Succès de la reliure par courriel","passwordModifiedSuccess":"Le mot de passe a été modifié avec succès","passwordSetSuccess":"Le mot de passe a été défini avec succès","oldPassword":"Ancien mot de passe :","inputCurrentPassword":"Saisis ton mot de passe actuel","newPassword":"Nouveau mot de passe :","inputNewPassword":"Saisis ton nouveau mot de passe","confirmNewPassword":"Confirme le nouveau mot de passe :","inputNewPasswordAgain":"Saisis à nouveau ton nouveau mot de passe","password":"Mot de passe :","modifyPassword":"Modifier le mot de passe","setPassword":"Définir le mot de passe","alreadySetPassword":"Mot de passe défini","setPassPlaceholder":"Tu peux te connecter avec ton mot de passe","setPassAfterBind":"Tu peux définir un mot de passe après la liaison du compte","socialConnections":"Connexions sociales"},"shortcut":{"shortcutList":"Raccourcis clavier","click":"Clique sur","global":"Global","toggleShortcutList":"Afficher les raccourcis clavier","editor":"Éditeur","toggleLeftPanel":"Basculer le volet gauche","toggleBottomPanel":"Basculer le volet inférieur","toggleRightPanel":"Basculer le volet droit","toggleAllPanels":"Basculer tous les panneaux","preview":"Aperçu","undo":"Annuler","redo":"Refaire","showGrid":"Afficher la grille","component":"Composant","multiSelect":"Sélectionne plusieurs","selectAll":"Sélectionner tout","copy":"Copie","cut":"Couper","paste":"Coller","move":"Déplacer","zoom":"Redimensionner","delete":"Supprimer","deSelect":"Désélectionne","queryEditor":"Éditeur de requêtes","excuteQuery":"Exécuter la requête actuelle","editBox":"Éditeur de texte","formatting":"Format","openInLeftPanel":"Ouvrir dans le volet gauche"},"help":{"videoText":"Vue d'ensemble","onBtnText":"OK","permissionDenyTitle":"💡 Impossible de créer une nouvelle application ou une nouvelle source de données ?","permissionDenyContent":"Tu n'as pas la permission de créer l'application et la source de données. Contacte l'administrateur pour rejoindre le groupe de développeurs.","appName":"Tutoriel d'application","chat":"Chat avec nous","docs":"Voir la documentation","editorTutorial":"Tutoriel de l'éditeur","update":"Quoi de neuf ?","version":"Version","versionWithColon":"Version : ","submitIssue":"Soumettre une question"},"header":{"nameCheckMessage":"Le nom ne peut pas être vide","viewOnly":"Voir seulement","recoverAppSnapshotTitle":"Restaurer cette version ?","recoverAppSnapshotContent":"Restaurer l'application actuelle à la version créée à {heure}.","recoverAppSnapshotMessage":"Restaurer cette version","returnEdit":"Retour à l'éditeur","deploy":"Publie","export":"Exporter vers JSON","editName":"Modifier le nom","duplicate":"Duplicata {type}","snapshot":"Histoire","scriptsAndStyles":"Scripts et style","appSettings":"Paramètres de l'application","preview":"Aperçu","editError":"Mode de prévisualisation de l'historique, aucune opération n'est prise en charge.","clone":"Clone","editorMode_layout":"Mise en page","editorMode_logic":"Logique","editorMode_both":"Les deux"},"userAuth":{"registerByEmail":"S'inscrire","email":"Courriel :","inputEmail":"Saisis ton courriel","inputValidEmail":"Saisis un courriel valide","register":"S'inscrire","userLogin":"S'inscrire","login":"S'inscrire","bind":"Relier","passwordCheckLength":"Au moins {min} Personnages","passwordCheckContainsNumberAndLetter":"Doit contenir des lettres et des chiffres","passwordCheckSpace":"Ne peut pas contenir de caractères d'espacement","welcomeTitle":"Bienvenue sur le site de {nom du produit}","inviteWelcomeTitle":"{nom d'utilisateur} T'invite à te connecter {nom du produit}","terms":"Conditions","privacy":"Politique de confidentialité","registerHint":"J'ai lu et j'accepte les","chooseAccount":"Choisis ton compte","signInLabel":"Se connecter avec {nom}","bindAccount":"Relier le compte","scanQrCode":"Scanne le code QR avec {nom}","invalidThirdPartyParam":"Param tiers invalide","account":"Compte","inputAccount":"Saisis ton compte","ldapLogin":"Connexion LDAP","resetPassword":"Réinitialiser le mot de passe","resetPasswordDesc":"Réinitialiser le mot de passe de l'utilisateur {nom}. Un nouveau mot de passe sera généré après la réinitialisation.","resetSuccess":"Réinitialisation réussie","resetSuccessDesc":"La réinitialisation du mot de passe a réussi. Le nouveau mot de passe est : {mot de passe}","copyPassword":"Copier le mot de passe","poweredByLowcoder":"Propulsé par Lowcoder.cloud"},"preLoad":{"jsLibraryHelpText":"Ajoute des bibliothèques JavaScript à ton application actuelle via des adresses URL. lodash, day.js, uuid, numbro sont intégrés au système pour une utilisation immédiate. Les bibliothèques JavaScript sont chargées avant l'initialisation de l'application, ce qui peut avoir un impact sur les performances de l'application.","exportedAs":"Exporté en tant que","urlTooltip":"Adresse URL de la bibliothèque JavaScript, [unpkg.com](https://unpkg.com/) ou [jsdelivr.net](https://www.jsdelivr.com/) est recommandée.","recommended":"Recommandé","viewJSLibraryDocument":"Document","jsLibraryURLError":"URL invalide","jsLibraryExist":"La bibliothèque JavaScript existe déjà","jsLibraryEmptyContent":"Aucune bibliothèque JavaScript n'a été ajoutée","jsLibraryDownloadError":"Erreur de téléchargement de la bibliothèque JavaScript","jsLibraryInstallSuccess":"La bibliothèque JavaScript a été installée avec succès","jsLibraryInstallFailed":"L'installation de la bibliothèque JavaScript a échoué","jsLibraryInstallFailedCloud":"La bibliothèque n'est peut-être pas disponible dans le bac à sable, [Documentation](https://docs.lowcoder.cloud/build-apps/write-javascript/use-third-party-libraries#manually-import-libraries)\\n{message}","jsLibraryInstallFailedHost":"{message}","add":"Ajouter un nouveau","jsHelpText":"Ajoute une méthode ou une variable globale à l'application en cours.","cssHelpText":"Ajouter des styles à l'application en cours. La structure du DOM peut changer au fur et à mesure de l'itération du système. Essayer de modifier les styles par le biais des propriétés des composants.","scriptsAndStyles":"Scripts et styles","jsLibrary":"Bibliothèque JavaScript"},"editorTutorials":{"component":"Composant","componentContent":"Le panneau des composants de droite te propose de nombreux blocs d'application (composants) prêts à l'emploi. Ceux-ci peuvent être glissés sur le canevas pour être utilisés. Tu peux aussi créer tes propres composants avec quelques connaissances en codage.","canvas":"Toile","canvasContent":"Construis tes applications sur le Canvas avec une approche \"Ce que tu vois est ce que tu obtiens\". Il te suffit de faire glisser et de déposer les composants pour concevoir ta mise en page, et d'utiliser les raccourcis clavier pour une édition rapide comme supprimer, copier et coller. Une fois qu'un composant est sélectionné, tu peux peaufiner chaque détail, du style et de la mise en page à la liaison des données et au comportement logique. De plus, tu bénéficies de l'avantage supplémentaire de la conception réactive, ce qui garantit que tes applications sont superbes sur n'importe quel appareil.","queryData":"Interroger les données","queryDataContent":"Tu peux créer des requêtes de données ici et te connecter à tes sources de données MySQL, MongoDB, Redis, Airtable et bien d'autres. Après avoir configuré la requête, clique sur \"Exécuter\" pour obtenir les données et poursuivre le tutoriel.","compProperties":"Propriétés des composants"},"homeTutorials":{"createAppContent":"🎉 Bienvenue sur {nom du produit}, clique sur \\'App\\' et commence à créer ta première application.","createAppTitle":"Créer une application"},"history":{"layout":"\\Ajustement de la mise en page \"{0}\\\".","upgrade":"Mise à niveau \"{0}\".","delete":"Supprimer \\N'{0}\\'","add":"Ajouter \"{0}\\","modify":"Modifier \\N- \"{0}\\N","rename":"Renomme \\N\"{1}\\N\" en \\N\"{0}\\N\".","recover":"Récupérer la version \"{2}\".","recoverVersion":"Récupérer la version","andSoOn":"et ainsi de suite","timeFormat":"MM DD à hh:mm A","emptyHistory":"Pas d'antécédents","currentVersionWithBracket":" (Actuel)","currentVersion":"Version actuelle","justNow":"Tout de suite","history":"Histoire"},"home":{"allApplications":"Toutes les applications","allModules":"Tous les modules","allFolders":"Tous les dossiers","modules":"Modules","module":"Module","trash":"Poubelle","queryLibrary":"Bibliothèque de requêtes","datasource":"Sources de données","selectDatasourceType":"Sélectionne le type de source de données","home":"Accueil | Zone d'administration","all":"Tous","app":"App","navigation":"Navigation","navLayout":"Navigation PC","navLayoutDesc":"Menu à gauche pour faciliter la navigation sur le bureau.","mobileTabLayout":"Navigation mobile","mobileTabLayoutDesc":"Barre de navigation inférieure pour une navigation mobile fluide.","folders":"Dossiers","folder":"Dossier","rootFolder":"Racine","import":"Importation","export":"Exporter vers JSON","inviteUser":"Invite les membres","createFolder":"Créer un dossier","createFolderSubTitle":"Nom du dossier :","moveToFolder":"Déplacer vers le dossier","moveToTrash":"Déplacer vers la poubelle","moveToFolderSubTitle":"Déplace-toi vers :","folderName":"Nom du dossier :","resCardSubTitle":"{heure} par {créateur}","trashEmpty":"La poubelle est vide.","projectEmpty":"Il n'y a rien ici.","projectEmptyCanAdd":"Tu n'as pas encore d'application. Clique sur Nouveau pour commencer.","name":"Nom","type":"Type","creator":"Créé par","lastModified":"Dernière modification","deleteTime":"Effacer l'heure","createTime":"Créer du temps","datasourceName":"Nom de la source de données","databaseName":"Nom de la base de données","nameCheckMessage":"Le nom ne peut pas être vide","deleteElementTitle":"Supprimer définitivement","moveToTrashSubTitle":"{type} {nom} sera déplacé dans la corbeille.","deleteElementSubTitle":"Supprimer {type} {nom} définitivement, il ne peut pas être récupéré.","deleteSuccessMsg":"Supprimé avec succès","deleteErrorMsg":"Erreur supprimée","recoverSuccessMsg":"Récupéré avec succès","newDatasource":"Nouvelle source de données","creating":"Créer...","chooseDataSourceType":"Choisis le type de source de données","folderAlreadyExists":"Le dossier existe déjà","newNavLayout":"{nom d'utilisateur}\\'s {name} ","newApp":"Le nouveau {nom d'utilisateur}\\Nest le nouveau {nom}. ","importError":"Erreur d'importation, {message}","exportError":"Erreur d'exportation, {message}","importSuccess":"Succès de l'importation","fileUploadError":"Erreur de téléchargement de fichier","fileFormatError":"Erreur de format de fichier","groupWithSquareBrackets":"[Groupe] ","allPermissions":"Propriétaire","shareLink":"Lien de partage : ","copyLink":"Copier le lien","appPublicMessage":"Rends l'application publique. Tout le monde peut la consulter.","modulePublicMessage":"Rends le module public. Tout le monde peut le consulter.","memberPermissionList":"Permissions aux membres : ","orgName":"{orgName} admins","addMember":"Ajouter des membres","addPermissionPlaceholder":"Saisis un nom pour rechercher des membres","searchMemberOrGroup":"Recherche des membres ou des groupes : ","addPermissionErrorMessage":"L'ajout d'une permission a échoué, {message}","copyModalTitle":"Clone-le","copyNameLabel":"{type} nom","copyModalfolderLabel":"Ajouter au dossier","copyNamePlaceholder":"Saisis un nom de {type}","chooseNavType":"Choisis le type de navigation","createNavigation":"Créer une navigation"},"carousel":{"dotPosition":"Position des points de navigation","autoPlay":"AutoPlay","showDots":"Afficher les points de navigation"},"npm":{"invalidNpmPackageName":"Nom ou URL de paquetage npm invalide.","pluginExisted":"Ce plugin npm existait déjà","compNotFound":"Le composant {compName} n'a pas été trouvé.","addPluginModalTitle":"Ajouter un plugin à partir d'un dépôt npm","pluginNameLabel":"URL ou nom du package npm","noCompText":"Pas de composants.","compsLoading":"Chargement...","removePluginBtnText":"Enlever","addPluginBtnText":"Ajouter un plugin npm"},"toggleButton":{"valueDesc":"La valeur par défaut du bouton à bascule, par exemple : Faux","trueDefaultText":"Cache-toi","falseDefaultText":"Montrer","trueLabel":"Texte pour True","falseLabel":"Texte pour Faux","trueIconLabel":"Icône pour True","falseIconLabel":"Icône pour Faux","iconPosition":"Position de l'icône","showText":"Afficher le texte","alignment":"Alignement","showBorder":"Afficher la bordure"},"componentDoc":{"markdownDemoText":"**Lowcoder** | Crée des applications logicielles pour ton entreprise et tes clients avec un minimum d'expérience en matière de codage. Lowcoder est la meilleure alternative à Retool, Appsmith ou Tooljet.","demoText":"Lowcoder | Crée des applications logicielles pour ton entreprise et tes clients avec une expérience minimale du codage. Lowcoder est la meilleure alternative à Retool, Appsmith ou Tooljet.","submit":"Soumettre","style":"Style","danger":"Danger","warning":"Avertissement","success":"Succès","menu":"Menu","link":"Lien","customAppearance":"Apparence personnalisée","search":"Recherche","pleaseInputNumber":"Saisis un numéro","mostValue":"La plus grande valeur","maxRating":"Valeur nominale maximale","notSelect":"Non sélectionné","halfSelect":"Demi-sélection","pleaseSelect":"Choisis","title":"Titre","content":"Contenu","componentNotFound":"Le composant n'existe pas","example":"Exemples","defaultMethodDesc":"Définir la valeur de la propriété {nom}","propertyUsage":"Tu peux lire les informations relatives aux composants en accédant aux propriétés des composants par leur nom partout où tu peux écrire du JavaScript.","property":"Propriétés","propertyName":"Nom de la propriété","propertyType":"Type","propertyDesc":"Description","event":"Événements","eventName":"Nom de l'événement","eventDesc":"Description","mehtod":"Méthodes","methodUsage":"Tu peux interagir avec les composants par le biais de leurs méthodes et tu peux les appeler par leur nom partout où tu peux écrire du JavaScript. Tu peux aussi les appeler par le biais de l'action \"Composant de contrôle\" d'un événement.","methodName":"Nom de la méthode","methodDesc":"Description","showBorder":"Afficher la bordure","haveTry":"Essaie toi-même","settings":"Réglage","settingValues":"Valeur de réglage","defaultValue":"Valeur par défaut","time":"L'heure","date":"Date","noValue":"Aucun","xAxisType":"Type d'axe X","hAlignType":"Alignement horizontal","leftLeftAlign":"Alignement gauche-gauche","leftRightAlign":"Alignement gauche-droite","topLeftAlign":"Alignement haut-gauche","topRightAlign":"Alignement haut-droit","validation":"Validation","required":"Exigée","defaultStartDateValue":"Date de début par défaut","defaultEndDateValue":"Date de fin par défaut","basicUsage":"Utilisation de base","basicDemoDescription":"Les exemples suivants montrent l'utilisation de base du composant.","noDefaultValue":"Pas de valeur par défaut","forbid":"Interdit","placeholder":"Placeholder","pleaseInputPassword":"Saisis ton mot de passe","password":"Mot de passe","textAlign":"Alignement du texte","length":"Longueur","top":"Haut","pleaseInputName":"Saisis ton nom","userName":"Nom","fixed":"Fixe","responsive":"Réactif","workCount":"Nombre de mots","cascaderOptions":"Options de cascade","pleaseSelectCity":"Choisis une ville","advanced":"Avancé","showClearIcon":"Afficher l'icône d'effacement","likedFruits":"Favoris","option":"Option","singleFileUpload":"Téléchargement d'un seul fichier","multiFileUpload":"Téléchargement de plusieurs fichiers","folderUpload":"Téléchargement de dossier","multiFile":"Fichiers multiples","folder":"Dossier","open":"Ouvrir","favoriteFruits":"Fruits préférés","pleaseSelectOneFruit":"Choisis un fruit","notComplete":"Pas complet","complete":"Complète","echart":"Diagramme","lineChart":"Graphique en ligne","basicLineChart":"Diagramme linéaire de base","lineChartType":"Type de graphique linéaire","stackLineChart":"Ligne empilée","areaLineChart":"Ligne de surface","scatterChart":"Diagramme de dispersion","scatterShape":"Forme de l'éparpillement","scatterShapeCircle":"Cercle","scatterShapeRect":"Rectangle","scatterShapeTri":"Triangle","scatterShapeDiamond":"Diamant","scatterShapePin":"Épingle à cheveux","scatterShapeArrow":"Flèche","pieChart":"Diagramme circulaire","basicPieChart":"Diagramme circulaire de base","pieChatType":"Type de diagramme à secteurs","pieChartTypeCircle":"Tableau des beignets","pieChartTypeRose":"Tableau des roses","titleAlign":"Titre Position","color":"Couleur","dashed":"En pointillé","imADivider":"Je suis une ligne de démarcation","tableSize":"Taille de la table","subMenuItem":"Sous-menu {num}","menuItem":"Menu {num}","labelText":"Étiquette","labelPosition":"Étiquette - Position","labelAlign":"Étiquette - Aligner","optionsOptionType":"Méthode de configuration","styleBackgroundColor":"Couleur de fond","styleBorderColor":"Couleur de la bordure","styleColor":"Couleur de la police","selectionMode":"Mode de sélection des rangées","paginationSetting":"Réglage de la pagination","paginationShowSizeChanger":"Aide les utilisateurs à modifier le nombre d'entrées par page","paginationShowSizeChangerButton":"Bouton de changement de taille","paginationShowQuickJumper":"Show Quick Jumper","paginationHideOnSinglePage":"Cacher lorsqu'il n'y a qu'une seule page","paginationPageSizeOptions":"Taille de la page","chartConfigCompType":"Type de graphique","xConfigType":"Type d'axe X","loading":"Chargement","disabled":"Désactivé","minLength":"Longueur minimale","maxLength":"Longueur maximale","showCount":"Afficher le nombre de mots","autoHeight":"Hauteur","thousandsSeparator":"Séparateur de milliers","precision":"Places décimales","value":"Valeur par défaut","formatter":"Format","min":"Valeur minimale","max":"Valeur maximale","step":"Taille de l'étape","start":"Heure de début","end":"L'heure de la fin","allowHalf":"Autoriser la sélection de la moitié","filetype":"Type de fichier","showUploadList":"Afficher la liste des téléchargements","uploadType":"Type de téléchargement","allowClear":"Afficher l'icône d'effacement","minSize":"Taille minimale du fichier","maxSize":"Taille maximale du fichier","maxFiles":"Nombre maximum de fichiers téléchargés","format":"Format","minDate":"Date minimum","maxDate":"Date maximale","minTime":"Durée minimale","maxTime":"Durée maximale","text":"Texte","type":"Type","hideHeader":"Cacher l'en-tête","hideBordered":"Cacher la bordure","src":"URL de l'image","showInfo":"Valeur d'affichage","mode":"Mode","onlyMenu":"Menu unique","horizontalAlignment":"Alignement horizontal","row":"Gauche","column":"Haut","leftAlign":"Alignement à gauche","rightAlign":"Alignement droit","percent":"Pourcentage","fixedHeight":"Hauteur fixe","auto":"Adaptatif","directory":"Dossier","multiple":"Fichiers multiples","singleFile":"Fichier unique","manual":"Manuel","default":"Défaut","small":"Petit","middle":"Moyen","large":"Grandes","single":"Célibataire","multi":"Multiple","close":"Fermer","ui":"Mode UI","line":"Graphique en ligne","scatter":"Diagramme de dispersion","pie":"Diagramme circulaire","basicLine":"Diagramme linéaire de base","stackedLine":"Tableau à lignes empilées","areaLine":"Zone Carte de la zone","basicPie":"Diagramme circulaire de base","doughnutPie":"Tableau des beignets","rosePie":"Tableau des roses","category":"Catégorie Axe","circle":"Cercle","rect":"Rectangle","triangle":"Triangle","diamond":"Diamant","pin":"Épingle à cheveux","arrow":"Flèche","left":"Gauche","right":"Droit","center":"Centre","bottom":"Le fond","justify":"Justifie les deux extrémités"},"playground":{"url":"https://app.lowcoder.cloud/playground/{compType}/1","data":"État actuel des données","preview":"Aperçu","property":"Propriétés","console":"Console Visual Script","executeMethods":"Exécuter les méthodes","noMethods":"Pas de méthodes.","methodParams":"Paramètres de la méthode","methodParamsHelp":"Paramètres de la méthode d'entrée à l'aide de JSON. Par exemple, tu peux définir les paramètres de la méthode avec : [1] ou 1"},"calendar":{"headerBtnBackground":"Arrière-plan des boutons","btnText":"Texte du bouton","title":"Titre","selectBackground":"Sélection d'antécédents"},"componentDocExtra":{"table":"Documentation supplémentaire pour le composant tableau"},"idSource":{"title":"Fournisseurs OAuth","form":"Courriel","pay":"Premium","enable":"Activer","unEnable":"Non activé","loginType":"Type de connexion","status":"Statut","desc":"Description","manual":"Carnet d'adresses :","syncManual":"Synchroniser le carnet d'adresses","syncManualSuccess":"Synchronisation réussie","enableRegister":"Autoriser l'enregistrement","saveBtn":"Sauvegarder et activer","save":"Sauvegarde","none":"Aucun","formPlaceholder":"Saisis {label}","formSelectPlaceholder":"Choisis le {label}","saveSuccess":"Sauvegardé avec succès","dangerLabel":"Zone de danger","dangerTip":"La désactivation de ce fournisseur d'identifiants peut entraîner l'impossibilité pour certains utilisateurs de se connecter. Procède avec prudence.","disable":"Désactiver","disableSuccess":"Désactivé avec succès","encryptedServer":"-------- Crypté du côté du serveur --------","disableTip":"Conseils","disableContent":"La désactivation de ce fournisseur d'identifiants peut entraîner l'impossibilité pour certains utilisateurs de se connecter. Es-tu sûr de vouloir continuer ?","manualTip":"","lockTip":"Le contenu est verrouillé. Pour apporter des modifications, clique sur l'icône pour le déverrouiller.","lockModalContent":"La modification du champ \"ID Attribute\" peut avoir des conséquences importantes sur l'identification de l'utilisateur. Confirme que tu comprends les implications de ce changement avant de continuer.","payUserTag":"Premium"},"slotControl":{"configSlotView":"Configurer la vue de l'emplacement"},"jsonLottie":{"lottieJson":"Lottie JSON","speed":"La vitesse","width":"Largeur","height":"Hauteur","backgroundColor":"Couleur de fond","animationStart":"Début de l'animation","valueDesc":"Données JSON actuelles","loop":"Boucle","auto":"Auto","onHover":"Au survol","singlePlay":"Jeu unique","endlessLoop":"Boucle sans fin","keepLastFrame":"Maintien de l'affichage de la dernière image"},"timeLine":{"titleColor":"Titre Couleur","subTitleColor":"Couleur des sous-titres","labelColor":"Couleur de l'étiquette","value":"Données chronologiques","mode":"Ordre d'affichage","left":"Droit au contenu","right":"Contenu à gauche","alternate":"Ordre alternatif du contenu","modeTooltip":"Régler le contenu pour qu'il apparaisse à gauche/droite ou alternativement des deux côtés de la ligne de temps","reverse":"Les événements les plus récents d'abord","pending":"Texte du nœud en attente","pendingDescription":"Lorsque cette option est activée, un dernier nœud avec le texte et un indicateur d'attente s'affichent.","defaultPending":"Amélioration continue","clickTitleEvent":"Clique sur Titre de l'événement","clickTitleEventDesc":"Clique sur Titre de l'événement","Introduction":"Introduction Clés","helpTitle":"Titre de la ligne du temps (obligatoire)","helpsubTitle":"Sous-titre de la chronologie","helpLabel":"Étiquette de la ligne de temps, utilisée pour afficher les dates","helpColor":"Indique la couleur du nœud de la ligne de temps","helpDot":"Rendre les nœuds de la ligne de temps sous forme d'icônes Ant Design","helpTitleColor":"Contrôle individuellement la couleur du titre du nœud","helpSubTitleColor":"Contrôle individuellement la couleur du sous-titre du nœud","helpLabelColor":"Contrôle individuellement la couleur de l'icône du nœud","valueDesc":"Données de la chronologie","clickedObjectDesc":"Données de l'élément cliqué","clickedIndexDesc":"Index des éléments cliqués"},"comment":{"value":"Données de la liste de commentaires","showSendButton":"Autoriser les commentaires","title":"Titre","titledDefaultValue":"%d Commentaire au total","placeholder":"Shift + Enter pour commenter ; Saisis @ ou # pour une saisie rapide","placeholderDec":"Placeholder","buttonTextDec":"Titre du bouton","buttonText":"Commentaire","mentionList":"Données de la liste des mentions","mentionListDec":"Mots clés ; Données de la liste des mentions de valeur","userInfo":"Info utilisateur","dateErr":"Erreur de date","commentList":"Liste des commentaires","deletedItem":"Point supprimé","submitedItem":"Article soumis","deleteAble":"Afficher le bouton de suppression","Introduction":"Introduction Clés","helpUser":"Informations sur l'utilisateur (obligatoire)","helpname":"Nom d'utilisateur (obligatoire)","helpavatar":"URL de l'avatar (Haute priorité)","helpdisplayName":"Nom d'affichage (faible priorité)","helpvalue":"Contenu des commentaires","helpcreatedAt":"Créer une date"},"mention":{"mentionList":"Données de la liste des mentions"},"autoComplete":{"value":"Auto Complete Value","checkedValueFrom":"Valeur vérifiée De","ignoreCase":"Recherche Ignorer le cas","searchLabelOnly":"Recherche sur l'étiquette uniquement","searchFirstPY":"Rechercher le premier pinyin","searchCompletePY":"Rechercher le pinyin complet","searchText":"Texte de recherche","SectionDataName":"Données d'auto-complétion","valueInItems":"Valeur en articles","type":"Type","antDesign":"AntDesign","normal":"Normal","selectKey":"Clé","selectLable":"Étiquette","ComponentType":"Type de composant","colorIcon":"Bleu","grewIcon":"Gris","noneIcon":"Aucun","small":"Petit","large":"Grandes","componentSize":"Taille du composant","Introduction":"Introduction Clés","helpLabel":"Étiquette","helpValue":"Valeur"},"responsiveLayout":{"column":"Colonnes","atLeastOneColumnError":"La mise en page responsive conserve au moins une colonne","columnsPerRow":"Colonnes par ligne","columnsSpacing":"Espacement des colonnes (px)","horizontal":"Horizontal","vertical":"Vertical","mobile":"Mobile","tablet":"Tablette","desktop":"Bureau","rowStyle":"Style de la rangée","columnStyle":"Style de colonne","minWidth":"Min. Largeur","rowBreak":"Pause dans les rangs","matchColumnsHeight":"Hauteur des colonnes","rowLayout":"Disposition des rangées","columnsLayout":"Disposition des colonnes"},"navLayout":{"mode":"Mode","modeInline":"En ligne","modeVertical":"Vertical","width":"Largeur","widthTooltip":"Pixel ou pourcentage, par exemple 520, 60%","navStyle":"Style de menu","navItemStyle":"Style de l'élément de menu"}} diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 0d200704f..434569141 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -293,7 +293,7 @@ themeDetail: { marginDesc: "默认外边距通常用于大多数组件", padding: "内边距", paddingDesc: "默认内边距通常用于大多数组件", - containerheaderpadding: "头部边距", + containerHeaderPadding: "头部边距", containerheaderpaddingDesc: "默认头部边距通常用于大多数组件", gridColumns: "网格列", gridColumnsDesc: @@ -352,9 +352,9 @@ style: { marginRight: "右外边距", marginTop: "上外边距", marginBottom: "下外边距", - containerheaderpadding: "上内边距", - containerfooterpadding: "下内边距", - containerbodypadding: "内边距", + containerHeaderPadding: "上内边距", + containerFooterPadding: "下内边距", + containerBodyPadding: "内边距", minWidth: "最小宽度", textSize: "字体大小", textWeight: "字体粗细", @@ -2599,7 +2599,7 @@ idSource: { timeLine: { titleColor: "标题颜色", subTitleColor: "子标题颜色", - lableColor: "标签颜色", + labelColor: "标签颜色", value: "数据", mode: "模式", left: "左", @@ -2619,7 +2619,7 @@ timeLine: { helpDot: "时间线的原点渲染成AntD的图标", helpTitleColor: "设置时间线标题颜色", helpSubTitleColor: "设置时间线子标题颜色", - helpLableColor: "设置时间线标签颜色", + helpLabelColor: "设置时间线标签颜色", valueDesc: "时间线的数据", clickedObjectDesc: "点击的项目数据", clickedIndexDesc: "点击的项目序号", diff --git a/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts b/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts index c00e98a2a..a868cca8f 100644 --- a/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts +++ b/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts @@ -508,7 +508,7 @@ const dsl = { style: { label: "", fill: "", - thumbBoder: "", + thumbBorder: "", thumb: "", track: "", }, From 53439456094f04b0038c00b9284a6086162966c8 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Wed, 13 Mar 2024 16:33:54 +0500 Subject: [PATCH 24/33] changed the API response to empty in case user does not exist. --- .../java/org/lowcoder/domain/user/service/UserServiceImpl.java | 2 +- .../java/org/lowcoder/api/usermanagement/UserController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 677a15a49..0eec56527 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -282,7 +282,7 @@ public Mono lostPassword(String userEmail) { String token = generateNewRandomPwd(); Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS); if (!emailCommunicationService.sendPasswordResetEmail(userEmail, token, emailTemplate)) { - return ofError(BizError.AUTH_ERROR, "SENDING_EMAIL_FAILED"); + return Mono.empty(); } user.setPasswordResetToken(HashUtils.hash(token.getBytes())); user.setPasswordResetTokenExpiry(tokenExpiry); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index ee4aac0bb..56cccffd5 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -149,7 +149,7 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques @Override public Mono> lostPassword(@RequestBody LostPasswordRequest request) { if (StringUtils.isBlank(request.userEmail())) { - return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER"); + return Mono.empty(); } return userApiService.lostPassword(request.userEmail()) .map(ResponseView::success); From 833c22bcf9ec29f3fff239692fb5966eb7ca9e42 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Wed, 13 Mar 2024 16:34:11 +0500 Subject: [PATCH 25/33] Added env Variables. --- .../src/main/resources/selfhost/ce/application.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index ca9b5b06a..44dd8a3f1 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -23,7 +23,15 @@ spring: port: ${LOWCODER_ADMIN_SMTP_PORT:587} username: ${LOWCODER_ADMIN_SMTP_USERNAME:yourmail@gmail.com} password: ${LOWCODER_ADMIN_SMTP_PASSWORD:yourpass} - + properties: + mail: + smtp: + auth: ${LOWCODER_ADMIN_SMTP_AUTH:true} + starttls: + enable: ${LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED:true} + required: ${LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED:true} + transport: + protocol: smtp server: compression: enabled: true From c5447c3a152034f68526ad658b53c176f9fa022b Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Thu, 14 Mar 2024 15:39:23 +0500 Subject: [PATCH 26/33] Added ssl auth. --- .../lowcoder-server/src/main/resources/application-lowcoder.yml | 2 ++ .../src/main/resources/selfhost/ce/application.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 61c9f527f..817cc0c4f 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -18,6 +18,8 @@ spring: mail: smtp: auth: true + ssl: + enable: false starttls: enable: true required: true diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 44dd8a3f1..7146b1d27 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -27,6 +27,8 @@ spring: mail: smtp: auth: ${LOWCODER_ADMIN_SMTP_AUTH:true} + ssl: + enable: ${LOWCODER_ADMIN_SMTP_SSL_ENABLED:false} starttls: enable: ${LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED:true} required: ${LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED:true} From e5613c5e8fb7697cdd359a8ccb28a89942e8220e Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Thu, 14 Mar 2024 19:33:51 +0500 Subject: [PATCH 27/33] updated email sender filed name --- .../lowcoder-server/src/main/resources/application-lowcoder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 817cc0c4f..39d69fe29 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -63,7 +63,7 @@ common: marketplace: private-mode: false lowcoder-public-url: http://localhost:8080 - lost-password-email-sender: info@lowcoder.org + notifications-email-sender: info@lowcoder.org material: mongodb-grid-fs: From a8b2ece63975baecb5189bdd4c79992ee118c8f4 Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Thu, 14 Mar 2024 19:35:13 +0500 Subject: [PATCH 28/33] updated email sender filed name --- .../src/main/resources/selfhost/ce/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 7146b1d27..6365dd8a7 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -71,7 +71,7 @@ common: marketplace: private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true} lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:8080} - lost-password-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:info@lowcoder.org} + notifications-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:info@lowcoder.org} material: mongodb-grid-fs: From d9d0883c09523c19f5e7e21ab46ed00fee7fcc4b Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Thu, 14 Mar 2024 20:48:22 +0500 Subject: [PATCH 29/33] fix crashing on reordering comps inside module --- .../src/pages/editor/LeftLayersContent.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx b/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx index 178e25138..911427601 100644 --- a/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx +++ b/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx @@ -33,6 +33,7 @@ import { import { DownOutlined } from "@ant-design/icons"; import { ItemType } from "antd/es/menu/hooks/useItems"; import ColorPicker, { configChangeParams } from "components/ColorPicker"; +import { ModuleLayoutComp } from "@lowcoder-ee/comps/comps/moduleContainerComp/moduleLayoutComp"; export type DisabledCollisionStatus = "true" | "false"; // "true" means collision is not enabled - Layering works, "false" means collision is enabled - Layering does not work @@ -209,17 +210,31 @@ export const LeftLayersContent = (props: LeftLayersContentProps) => { const dsl = editorState.rootComp.toJsonValue(); let layout: any = {}; - parentNode.children.forEach((data, index) => { - layout[data.key] = { - ...dsl.ui.layout[data.key], - pos: index, - }; - }) - - editorState.rootComp.children.ui.dispatchChangeValueAction({ - ...dsl.ui, - layout, - }) + if(dsl.ui.compType === 'module') { + parentNode.children.forEach((data, index) => { + layout[data.key] = { + ...dsl.ui.comp.container.layout[data.key], + pos: index, + }; + }) + const moduleLayoutComp = editorState.rootComp.children.ui.getModuleLayoutComp(); + moduleLayoutComp?.children.container.dispatchChangeValueAction({ + ...dsl.ui.comp.container, + layout, + }) + } else { + parentNode.children.forEach((data, index) => { + layout[data.key] = { + ...dsl.ui.layout[data.key], + pos: index, + }; + }) + + editorState.rootComp.children.ui.dispatchChangeValueAction({ + ...dsl.ui, + layout, + }) + } return newTreeData; }); } From 3af570bdc85f00a7e00cce4041a6649425483df6 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Thu, 14 Mar 2024 20:48:52 +0500 Subject: [PATCH 30/33] fix broken modal issue --- client/packages/lowcoder/src/comps/hooks/modalComp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx index fe9a6503a..9adc36385 100644 --- a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx @@ -161,7 +161,7 @@ let TmpModalComp = (function () { items={gridItemCompToGridItems(items)} autoHeight={props.autoHeight} minHeight={paddingValues ? DEFAULT_HEIGHT - paddingValues[0] * 2 + "px" : ""} - containerPadding={paddingValues ? [paddingValues[0], paddingValues[1]] : [24,24]} + containerPadding={paddingValues ? [paddingValues[0] ?? 0, paddingValues[1] ?? 0] : [24,24]} hintPlaceholder={HintPlaceHolder} /> From 8ef0a65924f2d1b30a0dcf83ecde452ba07cc8bc Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 15 Mar 2024 01:42:18 +0500 Subject: [PATCH 31/33] fix bulk action updates doesn't change all comps height --- client/packages/lowcoder/src/layout/gridLayout.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/layout/gridLayout.tsx b/client/packages/lowcoder/src/layout/gridLayout.tsx index 83facb75e..9dec97a2d 100644 --- a/client/packages/lowcoder/src/layout/gridLayout.tsx +++ b/client/packages/lowcoder/src/layout/gridLayout.tsx @@ -400,8 +400,15 @@ class GridLayout extends React.Component { // const ops = layoutOpUtils.push(this.state.ops, stickyItemOp(i, { h })); // this.setState({ ops }); if (this.state.changedHs?.[i] !== h) { - const changedHeights = { ...this.state.changedHs, [i]: h }; - this.setState({ changedHs: changedHeights }); + this.setState((prevState) => { + return { + ...prevState, + changedHs: { + ...prevState.changedHs, + [i]: h, + } + } + }) } }; From 9137090458d4f09c62b273c04bda8747ab8393e5 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 15 Mar 2024 02:03:51 +0500 Subject: [PATCH 32/33] fix iconSelector issue in js --- client/packages/lowcoder/src/comps/controls/iconControl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/controls/iconControl.tsx b/client/packages/lowcoder/src/comps/controls/iconControl.tsx index ca954b47d..e33688510 100644 --- a/client/packages/lowcoder/src/comps/controls/iconControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconControl.tsx @@ -165,7 +165,7 @@ function IconCodeEditor(props: { visible={visible} setVisible={setVisible} trigger="contextMenu" - parent={document.querySelector(`${CodeEditorTooltipContainer}`)} + // parent={document.querySelector(`${CodeEditorTooltipContainer}`)} searchKeywords={i18nObjs.iconSearchKeywords} /> ), From cdbbb6ccaf0e7dc5cc33d916d53bd752e549c2e8 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Thu, 14 Mar 2024 23:57:40 +0100 Subject: [PATCH 33/33] Just a small remark for later Todos --- client/packages/lowcoder/src/comps/utils/gridCompOperator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts b/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts index 828b8f286..6175e2794 100644 --- a/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts +++ b/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts @@ -77,6 +77,7 @@ export class GridCompOperator { return true; } + // FALK TODO: How can we enable Copy and Paste of components across Browser Tabs / Windows? static pasteComp(editorState: EditorState) { if (!this.copyComps || _.size(this.copyComps) <= 0 || !this.sourcePositionParams) { messageInstance.info(trans("gridCompOperator.selectCompFirst"));