mirror of https://github.com/halo-dev/halo
feat: add verification function to the notification settings for the mailbox (#5464)
#### What type of PR is this? /kind feature /area ui /area core /milestone 2.14.x #### What this PR does / why we need it: 为邮件的 `通知设置` 添加验证的功能。 同时为 formkit 增加了一个新的组件 (verificationForm),用于支持验证,它的定义方式如下: ``` - $formkit: verificationForm action: "http://localhost:8090/verify/user" label: 用户验证 children: - $formkit: text label: "用户名" name: username validation: required - $formkit: password label: "密码" name: password validation: required ``` verificationForm 支持 `action` 属性,当前端数据验证通过时,会将其下所包含的子节点数据发送至 action 所代表的接口上。 按上述示例,则验证数据会提交至 `http://localhost:8090/verify/user` 进行验证。验证的数据为 `{name: xxx, password: xxx}` 需要注意的是,verificationForm 只用于包装需要验证的数据,不会破坏原始数据的格式。因此上述数据在提交保存后仍旧为 `{name: xxx, password: xxx}` 而不会变成 `{verificationForm1: {name: xxx, password: xxx}}` #### How to test it? 1. 测试邮箱中的 `通知设置` 新增的验证按钮是否可以正常验证邮箱。 2. 查看数据是否正常回显 #### Which issue(s) this PR fixes: Fixes #4714 #### Does this PR introduce a user-facing change? ```release-note 在邮件通知设置中增加了发送测试的功能。 ```pull/5596/head
parent
78c00a28f3
commit
a2810156da
|
@ -1,23 +1,21 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.mail.javamail.MimeMessagePreparator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.notification.EmailSenderHelper.EmailSenderConfig;
|
||||
|
||||
/**
|
||||
* <p>A notifier that can send email.</p>
|
||||
|
@ -34,7 +32,8 @@ public class EmailNotifier implements ReactiveNotifier {
|
|||
|
||||
private final SubscriberEmailResolver subscriberEmailResolver;
|
||||
private final NotificationTemplateRender notificationTemplateRender;
|
||||
private final AtomicReference<Pair<EmailSenderConfig, JavaMailSenderImpl>>
|
||||
private final EmailSenderHelper emailSenderHelper;
|
||||
private final AtomicReference<Pair<EmailSenderConfig, JavaMailSender>>
|
||||
emailSenderConfigPairRef = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
|
@ -48,7 +47,7 @@ public class EmailNotifier implements ReactiveNotifier {
|
|||
return Mono.empty();
|
||||
}
|
||||
|
||||
JavaMailSenderImpl javaMailSender = getJavaMailSender(emailSenderConfig);
|
||||
JavaMailSender javaMailSender = getJavaMailSender(emailSenderConfig);
|
||||
|
||||
String recipient = context.getMessage().getRecipient();
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
|
@ -83,55 +82,20 @@ public class EmailNotifier implements ReactiveNotifier {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
private static MimeMessagePreparator getMimeMessagePreparator(String toEmail,
|
||||
private MimeMessagePreparator getMimeMessagePreparator(String toEmail,
|
||||
EmailSenderConfig emailSenderConfig, NotificationContext.MessagePayload payload) {
|
||||
return mimeMessage -> {
|
||||
MimeMessageHelper helper =
|
||||
new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name());
|
||||
helper.setFrom(emailSenderConfig.getSender(), emailSenderConfig.getDisplayName());
|
||||
|
||||
helper.setSubject(payload.getTitle());
|
||||
helper.setText(payload.getRawBody(), payload.getHtmlBody());
|
||||
helper.setTo(toEmail);
|
||||
};
|
||||
return emailSenderHelper.createMimeMessagePreparator(emailSenderConfig, toEmail,
|
||||
payload.getTitle(),
|
||||
payload.getRawBody(), payload.getHtmlBody());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static JavaMailSenderImpl createJavaMailSender(EmailSenderConfig emailSenderConfig) {
|
||||
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
|
||||
javaMailSender.setHost(emailSenderConfig.getHost());
|
||||
javaMailSender.setPort(emailSenderConfig.getPort());
|
||||
javaMailSender.setUsername(emailSenderConfig.getUsername());
|
||||
javaMailSender.setPassword(emailSenderConfig.getPassword());
|
||||
|
||||
Properties props = javaMailSender.getJavaMailProperties();
|
||||
props.put("mail.transport.protocol", "smtp");
|
||||
props.put("mail.smtp.auth", "true");
|
||||
if ("SSL".equals(emailSenderConfig.getEncryption())) {
|
||||
props.put("mail.smtp.ssl.enable", "true");
|
||||
}
|
||||
|
||||
if ("TLS".equals(emailSenderConfig.getEncryption())) {
|
||||
props.put("mail.smtp.starttls.enable", "true");
|
||||
}
|
||||
|
||||
if ("NONE".equals(emailSenderConfig.getEncryption())) {
|
||||
props.put("mail.smtp.ssl.enable", "false");
|
||||
props.put("mail.smtp.starttls.enable", "false");
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
props.put("mail.debug", "true");
|
||||
}
|
||||
return javaMailSender;
|
||||
}
|
||||
|
||||
JavaMailSenderImpl getJavaMailSender(EmailSenderConfig emailSenderConfig) {
|
||||
JavaMailSender getJavaMailSender(EmailSenderConfig emailSenderConfig) {
|
||||
return emailSenderConfigPairRef.updateAndGet(pair -> {
|
||||
if (pair != null && pair.getFirst().equals(emailSenderConfig)) {
|
||||
return pair;
|
||||
}
|
||||
return Pair.of(emailSenderConfig, createJavaMailSender(emailSenderConfig));
|
||||
return Pair.of(emailSenderConfig,
|
||||
emailSenderHelper.createJavaMailSender(emailSenderConfig));
|
||||
}).getSecond();
|
||||
}
|
||||
|
||||
|
@ -156,34 +120,4 @@ public class EmailNotifier implements ReactiveNotifier {
|
|||
</div>
|
||||
""", attributes);
|
||||
}
|
||||
|
||||
@Data
|
||||
static class EmailSenderConfig {
|
||||
private boolean enable;
|
||||
private String displayName;
|
||||
private String username;
|
||||
private String password;
|
||||
private String host;
|
||||
private Integer port;
|
||||
private String encryption;
|
||||
private String sender;
|
||||
|
||||
/**
|
||||
* Gets email display name.
|
||||
*
|
||||
* @return display name if not blank, otherwise username.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return StringUtils.defaultIfBlank(displayName, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets email sender address.
|
||||
*
|
||||
* @return sender if not blank, otherwise username.
|
||||
*/
|
||||
public String getSender() {
|
||||
return StringUtils.defaultIfBlank(sender, username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessagePreparator;
|
||||
|
||||
public interface EmailSenderHelper {
|
||||
|
||||
@NonNull
|
||||
JavaMailSender createJavaMailSender(EmailSenderConfig senderConfig);
|
||||
|
||||
@NonNull
|
||||
MimeMessagePreparator createMimeMessagePreparator(EmailSenderConfig senderConfig,
|
||||
String toEmail, String subject, String raw, String html);
|
||||
|
||||
@Data
|
||||
class EmailSenderConfig {
|
||||
private boolean enable;
|
||||
private String displayName;
|
||||
private String username;
|
||||
private String sender;
|
||||
private String password;
|
||||
private String host;
|
||||
private Integer port;
|
||||
private String encryption;
|
||||
|
||||
/**
|
||||
* Gets email display name.
|
||||
*
|
||||
* @return display name if not blank, otherwise username.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return StringUtils.defaultIfBlank(displayName, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets email sender address.
|
||||
*
|
||||
* @return sender if not blank, otherwise username
|
||||
*/
|
||||
public String getSender() {
|
||||
return StringUtils.defaultIfBlank(sender, username);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.mail.javamail.MimeMessagePreparator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* <p>A default implementation of {@link EmailSenderHelper}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EmailSenderHelperImpl implements EmailSenderHelper {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public JavaMailSender createJavaMailSender(EmailSenderConfig senderConfig) {
|
||||
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
|
||||
javaMailSender.setHost(senderConfig.getHost());
|
||||
javaMailSender.setPort(senderConfig.getPort());
|
||||
javaMailSender.setUsername(senderConfig.getUsername());
|
||||
javaMailSender.setPassword(senderConfig.getPassword());
|
||||
|
||||
Properties props = javaMailSender.getJavaMailProperties();
|
||||
props.put("mail.transport.protocol", "smtp");
|
||||
props.put("mail.smtp.auth", "true");
|
||||
if ("SSL".equals(senderConfig.getEncryption())) {
|
||||
props.put("mail.smtp.ssl.enable", "true");
|
||||
}
|
||||
|
||||
if ("TLS".equals(senderConfig.getEncryption())) {
|
||||
props.put("mail.smtp.starttls.enable", "true");
|
||||
}
|
||||
|
||||
if ("NONE".equals(senderConfig.getEncryption())) {
|
||||
props.put("mail.smtp.ssl.enable", "false");
|
||||
props.put("mail.smtp.starttls.enable", "false");
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
props.put("mail.debug", "true");
|
||||
}
|
||||
return javaMailSender;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public MimeMessagePreparator createMimeMessagePreparator(EmailSenderConfig senderConfig,
|
||||
String toEmail, String subject, String raw, String html) {
|
||||
return mimeMessage -> {
|
||||
MimeMessageHelper helper =
|
||||
new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name());
|
||||
helper.setFrom(senderConfig.getSender(), senderConfig.getDisplayName());
|
||||
|
||||
helper.setSubject(subject);
|
||||
helper.setText(raw, html);
|
||||
helper.setTo(toEmail);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package run.halo.app.notification.endpoint;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.security.Principal;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.notification.EmailSenderHelper;
|
||||
|
||||
/**
|
||||
* Validation endpoint for email config.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EmailConfigValidationEndpoint implements CustomEndpoint {
|
||||
private static final String EMAIL_SUBJECT = "测试邮件 - 验证邮箱连通性";
|
||||
private static final String EMAIL_BODY = """
|
||||
你好!<br/>
|
||||
这是一封测试邮件,旨在验证您的邮箱发件配置是否正确。<br/>
|
||||
此邮件由系统自动发送,请勿回复。<br/>
|
||||
祝好
|
||||
""";
|
||||
|
||||
private final EmailSenderHelper emailSenderHelper;
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
var tag = "console.api.notification.halo.run/v1alpha1/Notifier";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.POST("/notifiers/default-email-notifier/verify-connection",
|
||||
this::verifyEmailSenderConfig,
|
||||
builder -> builder.operationId("VerifyEmailSenderConfig")
|
||||
.description("Verify email sender config.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.implementation(ValidationRequest.class)
|
||||
)
|
||||
.response(responseBuilder().implementation(Void.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> verifyEmailSenderConfig(ServerRequest request) {
|
||||
return request.bodyToMono(ValidationRequest.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new ServerWebInputException("Required request body is missing."))
|
||||
)
|
||||
.flatMap(validationRequest -> getCurrentUserEmail()
|
||||
.flatMap(recipient -> {
|
||||
var mailSender = emailSenderHelper.createJavaMailSender(validationRequest);
|
||||
var message = emailSenderHelper.createMimeMessagePreparator(validationRequest,
|
||||
recipient, EMAIL_SUBJECT, EMAIL_BODY, EMAIL_BODY);
|
||||
try {
|
||||
mailSender.send(message);
|
||||
} catch (MailException e) {
|
||||
String errorMsg =
|
||||
"Failed to send email, please check your email configuration.";
|
||||
log.error(errorMsg, e);
|
||||
throw new ServerWebInputException(errorMsg, null, e);
|
||||
}
|
||||
return ServerResponse.ok().build();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Mono<String> getCurrentUserEmail() {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Principal::getName)
|
||||
.flatMap(username -> client.fetch(User.class, username))
|
||||
.flatMap(user -> {
|
||||
var email = user.getSpec().getEmail();
|
||||
if (StringUtils.isBlank(email)) {
|
||||
return Mono.error(new ServerWebInputException(
|
||||
"Your email is missing, please set it in your profile."));
|
||||
}
|
||||
return Mono.just(email);
|
||||
});
|
||||
}
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "EmailConfigValidationRequest")
|
||||
static class ValidationRequest extends EmailSenderHelper.EmailSenderConfig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("console.api.notification.halo.run/v1alpha1");
|
||||
}
|
||||
}
|
|
@ -23,47 +23,46 @@ spec:
|
|||
label: "启用邮件通知器"
|
||||
value: false
|
||||
name: enable
|
||||
- $formkit: text
|
||||
- $formkit: verificationForm
|
||||
if: "$enable"
|
||||
label: "用户名"
|
||||
name: username
|
||||
validation: required
|
||||
- $formkit: text
|
||||
if: "$enable"
|
||||
label: "发信地址"
|
||||
name: "sender"
|
||||
help: "如果用户名为实际发信地址,可忽略"
|
||||
- $formkit: password
|
||||
if: "$enable"
|
||||
label: "密码"
|
||||
name: password
|
||||
validation: required
|
||||
- $formkit: text
|
||||
if: "$enable"
|
||||
label: "显示名称"
|
||||
name: displayName
|
||||
- $formkit: text
|
||||
if: "$enable"
|
||||
label: "SMTP 服务器地址"
|
||||
name: host
|
||||
validation: required
|
||||
- $formkit: text
|
||||
if: "$enable"
|
||||
label: "端口号"
|
||||
name: port
|
||||
validation: required
|
||||
- $formkit: select
|
||||
if: "$enable"
|
||||
label: "加密方式"
|
||||
name: encryption
|
||||
value: "SSL"
|
||||
options:
|
||||
- label: "SSL"
|
||||
action: /apis/console.api.notification.halo.run/v1alpha1/notifiers/default-email-notifier/verify-connection
|
||||
label: 测试邮箱
|
||||
children:
|
||||
- $formkit: text
|
||||
label: "用户名"
|
||||
name: username
|
||||
validation: required
|
||||
- $formkit: text
|
||||
if: "$enable"
|
||||
label: "发信地址"
|
||||
name: "sender"
|
||||
help: "如果用户名为实际发信地址,可忽略"
|
||||
- $formkit: password
|
||||
label: "密码"
|
||||
name: password
|
||||
validation: required
|
||||
- $formkit: text
|
||||
label: "显示名称"
|
||||
name: displayName
|
||||
- $formkit: text
|
||||
label: "SMTP 服务器地址"
|
||||
name: host
|
||||
validation: required
|
||||
- $formkit: text
|
||||
label: "端口号"
|
||||
name: port
|
||||
validation: required
|
||||
- $formkit: select
|
||||
label: "加密方式"
|
||||
name: encryption
|
||||
value: "SSL"
|
||||
- label: "TLS"
|
||||
value: "TLS"
|
||||
- label: "不加密"
|
||||
value: "NONE"
|
||||
options:
|
||||
- label: "SSL"
|
||||
value: "SSL"
|
||||
- label: "TLS"
|
||||
value: "TLS"
|
||||
- label: "不加密"
|
||||
value: "NONE"
|
||||
---
|
||||
apiVersion: notification.halo.run/v1alpha1
|
||||
kind: ReasonType
|
||||
|
|
|
@ -15,3 +15,7 @@ rules:
|
|||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "notifiers/sender-config" ]
|
||||
verbs: [ "get", "update" ]
|
||||
- apiGroups: [ "console.api.notification.halo.run" ]
|
||||
resources: [ "notifiers/verify-connection" ]
|
||||
resourceNames: [ "default-email-notifier" ]
|
||||
verbs: [ "create" ]
|
||||
|
|
|
@ -38,6 +38,11 @@
|
|||
- 参数
|
||||
1. multiple: 是否多选,默认为 `false`
|
||||
- `tagCheckbox`:选择多个标签
|
||||
- `verificationForm`: 远程验证一组数据是否符合某个规则
|
||||
- 参数
|
||||
1. action: 对目标数据进行验证的接口地址
|
||||
2. label: 验证按钮文本
|
||||
3. buttonAttrs: 验证按钮的额外属性
|
||||
|
||||
在 Vue 单组件中使用:
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import { roleSelect } from "./inputs/role-select";
|
|||
import { attachmentPolicySelect } from "./inputs/attachment-policy-select";
|
||||
import { attachmentGroupSelect } from "./inputs/attachment-group-select";
|
||||
import { password } from "./inputs/password";
|
||||
import { verificationForm } from "./inputs/verify-form";
|
||||
|
||||
import radioAlt from "./plugins/radio-alt";
|
||||
import stopImplicitSubmission from "./plugins/stop-implicit-submission";
|
||||
|
@ -59,6 +60,7 @@ const config: DefaultConfigOptions = {
|
|||
roleSelect,
|
||||
attachmentPolicySelect,
|
||||
attachmentGroupSelect,
|
||||
verificationForm,
|
||||
},
|
||||
locales: { zh, en },
|
||||
locale: "zh",
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
VButton,
|
||||
Toast,
|
||||
IconCheckboxCircle,
|
||||
IconErrorWarning,
|
||||
} from "@halo-dev/components";
|
||||
import { createMessage, type FormKitFrameworkContext } from "@formkit/core";
|
||||
import type { PropType } from "vue";
|
||||
import { i18n } from "@/locales";
|
||||
import { axiosInstance } from "@/utils/api-client";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
context: {
|
||||
type: Object as PropType<FormKitFrameworkContext>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const loadingState = ref<boolean>(false);
|
||||
const stateMessage = ref<{
|
||||
state: "default" | "success" | "error";
|
||||
message?: string;
|
||||
}>({
|
||||
state: "default",
|
||||
});
|
||||
|
||||
const loading = createMessage({
|
||||
key: "loading",
|
||||
value: true,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle the verify event.
|
||||
*
|
||||
* @param e - The event
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
async function handleVerification(event: Event) {
|
||||
const node = props.context.node;
|
||||
event.preventDefault();
|
||||
await node.settled;
|
||||
|
||||
if (!node.ledger.value("blocking")) {
|
||||
verifyAction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* verify action
|
||||
* @param node
|
||||
*/
|
||||
function verifyAction() {
|
||||
const node = props.context.node;
|
||||
const action = node.props.action;
|
||||
if (!action) {
|
||||
const message = i18n.global.t(
|
||||
"core.formkit.verification_form.no_action_defined",
|
||||
{
|
||||
label: node.props.label,
|
||||
}
|
||||
);
|
||||
stateMessage.value = {
|
||||
state: "error",
|
||||
message,
|
||||
};
|
||||
Toast.error(message);
|
||||
return;
|
||||
}
|
||||
loadingState.value = true;
|
||||
const val = node.value as Record<string, unknown>;
|
||||
node.store.set(loading);
|
||||
// call onSubmit
|
||||
axiosInstance
|
||||
.post(action, val)
|
||||
.then(() => {
|
||||
stateMessage.value = {
|
||||
state: "success",
|
||||
};
|
||||
Toast.success(
|
||||
i18n.global.t("core.formkit.verification_form.verify_success", {
|
||||
label: node.props.label,
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
stateMessage.value = {
|
||||
state: "error",
|
||||
};
|
||||
const errorResponse = error.response;
|
||||
const { title, detail } = errorResponse.data;
|
||||
if (title || detail) {
|
||||
stateMessage.value.message = detail || title;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
node.store.remove("loading");
|
||||
loadingState.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const node = props.context.node;
|
||||
node.on("commit", () => {
|
||||
stateMessage.value = {
|
||||
state: "default",
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="context.classes.submit" class="py-4" @click="handleVerification">
|
||||
<VButton
|
||||
v-tooltip="stateMessage.message"
|
||||
:disabled="context.node.props.buttonAttrs.disabled"
|
||||
:loading="loadingState"
|
||||
>
|
||||
{{ context.node.props.label }}
|
||||
<template v-if="stateMessage.state !== 'default'" #icon>
|
||||
<IconCheckboxCircle
|
||||
v-if="stateMessage.state === 'success'"
|
||||
class="h-full w-full text-green-500"
|
||||
/>
|
||||
<IconErrorWarning
|
||||
v-else-if="stateMessage.state === 'error'"
|
||||
class="h-full w-full text-red-500"
|
||||
/>
|
||||
</template>
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,58 @@
|
|||
import { i18n } from "@/locales";
|
||||
import { type FormKitNode } from "@formkit/core";
|
||||
|
||||
function buildVerifyFormValue(node: FormKitNode) {
|
||||
if (!node.parent) return {};
|
||||
const parentValue = {
|
||||
...(node.parent.value as Record<string, unknown>),
|
||||
};
|
||||
delete parentValue[node.name];
|
||||
return parentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A feature to add a submit handler and actions section.
|
||||
*
|
||||
* @param node - A {@link @formkit/core#FormKitNode | FormKitNode}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default function verify(node: FormKitNode): void {
|
||||
node.props.buttonAttrs ??= {
|
||||
disabled: node.props.disabled,
|
||||
};
|
||||
|
||||
node.props.label ??= i18n.global.t("core.common.buttons.verify");
|
||||
|
||||
node.on("prop:disabled", ({ payload: disabled }) => {
|
||||
node.props.buttonAttrs = { ...node.props.buttonAttrs, disabled };
|
||||
});
|
||||
|
||||
node.on("created", () => {
|
||||
if (node.parent) {
|
||||
node.parent.hook.commit((val) => {
|
||||
const parentValue = {
|
||||
...val,
|
||||
};
|
||||
const verifyFormVal = (parentValue[node.name] || {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
delete parentValue[node.name];
|
||||
const mergeFormValue = {};
|
||||
Object.keys(verifyFormVal).forEach((key) => {
|
||||
if (node.children.find((child) => child.name === key)) {
|
||||
mergeFormValue[key] = verifyFormVal[key];
|
||||
}
|
||||
});
|
||||
return { ...parentValue, ...mergeFormValue };
|
||||
});
|
||||
|
||||
node.hook.input(() => {
|
||||
return buildVerifyFormValue(node);
|
||||
});
|
||||
|
||||
node.input(buildVerifyFormValue(node));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import type { FormKitTypeDefinition } from "@formkit/core";
|
||||
import {
|
||||
createSection,
|
||||
disablesChildren,
|
||||
message,
|
||||
messages,
|
||||
} from "@formkit/inputs";
|
||||
import { default as verifyFeature } from "./features";
|
||||
import VerificationButton from "./VerificationButton.vue";
|
||||
|
||||
export const verifyInput = createSection("verificationForm", () => ({
|
||||
$el: "div",
|
||||
bind: "$attrs",
|
||||
attrs: {
|
||||
id: "$id",
|
||||
name: "$node.name",
|
||||
"data-loading": "$state.loading || undefined",
|
||||
},
|
||||
}));
|
||||
|
||||
export const actions = createSection("actions", () => ({
|
||||
$el: "div",
|
||||
}));
|
||||
|
||||
export const verificationButton = createSection("verificationButton", () => ({
|
||||
$cmp: "VerificationButton",
|
||||
type: "button",
|
||||
bind: "$buttonAttrs",
|
||||
props: {
|
||||
context: "$node.context",
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Input definition for a form.
|
||||
* @public
|
||||
*/
|
||||
export const verificationForm: FormKitTypeDefinition = {
|
||||
/**
|
||||
* The actual schema of the input, or a function that returns the schema.
|
||||
*/
|
||||
schema: verifyInput(
|
||||
"$slots.default",
|
||||
messages(message("$message.value")),
|
||||
actions(verificationButton())
|
||||
),
|
||||
/**
|
||||
* The type of node, can be a list, group, or input.
|
||||
*/
|
||||
type: "group",
|
||||
/**
|
||||
* An array of extra props to accept for this input.
|
||||
*/
|
||||
props: ["action", "label", "buttonAttrs"],
|
||||
|
||||
/**
|
||||
* Additional features that should be added to your input
|
||||
*/
|
||||
features: [verifyFeature, disablesChildren],
|
||||
|
||||
library: {
|
||||
VerificationButton,
|
||||
},
|
||||
};
|
|
@ -33,6 +33,7 @@ const theme: Record<string, Record<string, string>> = {
|
|||
help: "text-xs mt-2 text-gray-500",
|
||||
messages: "list-none p-0 mt-1.5 mb-0 transition-all",
|
||||
message: "text-red-500 mt-2 text-xs",
|
||||
verificationForm: "pt-4 divide-y divide-gray-100",
|
||||
},
|
||||
button: buttonClassification,
|
||||
color: {
|
||||
|
|
|
@ -1482,6 +1482,10 @@ core:
|
|||
creation_label: Create {text} tag
|
||||
validation:
|
||||
trim: Please remove the leading and trailing spaces
|
||||
verification_form:
|
||||
no_action_defined: "{label} interface not defined"
|
||||
verify_success: "{label} successful"
|
||||
verify_failed: "{label} failed"
|
||||
common:
|
||||
buttons:
|
||||
save: Save
|
||||
|
|
|
@ -1428,6 +1428,10 @@ core:
|
|||
creation_label: 创建 {text} 标签
|
||||
validation:
|
||||
trim: 不能以空格开头或结尾
|
||||
verification_form:
|
||||
no_action_defined: 未定义{label}接口
|
||||
verify_success: "{label}成功"
|
||||
verify_failed: "{label}失败"
|
||||
common:
|
||||
buttons:
|
||||
save: 保存
|
||||
|
|
|
@ -1394,6 +1394,10 @@ core:
|
|||
creation_label: 創建 {text} 標籤
|
||||
validation:
|
||||
trim: 不能以空格開頭或結尾
|
||||
verification_form:
|
||||
no_action_defined: 未定義{label}介面
|
||||
verify_success: "{label}成功"
|
||||
verify_failed: "{label}失敗"
|
||||
common:
|
||||
buttons:
|
||||
save: 保存
|
||||
|
|
Loading…
Reference in New Issue