Este é um artigo para futuros desenvolvedores que podem precisar dele para criar plugins personalizados especificamente para validação de campos personalizados de usuário.
Escrito com a versão 3.6.0.beta3-latest do Discourse (commit atual a7326abf15), então se algo não funcionar, pode ser que algo tenha mudado no código principal.
O principal motivo para escrever este artigo é a ausência absoluta de informações sobre como adicionar validação personalizada aos campos personalizados de usuário.
A história curta é que tive que adicionar uma validação personalizada para um dos campos personalizados de usuário, que é basicamente uma entrada de texto livre. O principal requisito era que este campo tivesse um valor único. Sendo um desenvolvedor Python/PHP, é muito fácil implementar nos frameworks/CMS com os quais trabalho, enquanto o Discourse não era o caso e não se trata de especificidades da linguagem, mas do fato de que não há documentação para fazer isso e as postagens que encontrei com perguntas semelhantes que poderiam ajudar não foram respondidas. Após meio semana de pesquisa no código principal, finalmente alcancei o resultado esperado e quero compartilhá-lo com outros, pois pode ajudar alguém.
Portanto, um campo personalizado de usuário que deve ter um valor único.
Para resolver isso, precisamos criar um plugin personalizado para o Discourse, o principal motivo para isso é que precisamos verificar o valor no banco de dados, o que é feito no backend.
Aqui está a estrutura:
- meu_plugin_personalizado
-- assets
--- javascripts
---- discourse
----- initializers
------ seu-arquivo-js-inicializador.js
-- config
--- locales
---- server.en.yml
---- client.en.yml
--- settings.yml
-- plugin.rb
Agora, o principal problema que enfrentei foi como injetar nos campos personalizados. Depois de pesquisar o código principal, encontrei o método api addCustomUserFieldValidationCallback, ele pode ser encontrado no arquivo plugin-api.gjs no código principal. Este é um método que dispara após a primeira tentativa de enviar o formulário de inscrição e, posteriormente, dispara a cada digitação no campo de entrada ou em qualquer campo personalizado de usuário, que é o que eu precisava.
No exemplo para este método, ele mostra que aceita uma função de callback e o argumento que ele retorna é o próprio userField com o qual você está interagindo no momento.
Aqui está o código para seu-arquivo-js-inicializador.js (não completo, mas o suficiente):
import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";
export default apiInitializer("1.8.0", (api) => {
// Lista de IDs de campos que exigem validação única
const UNIQUE_FIELD_IDS = ["5"];
// Callback de validação do lado do cliente
api.addCustomUserFieldValidationCallback((userField) => {
const fieldId = userField.field.id.toString();
// Valida apenas os campos em nossa lista de campos únicos
if (UNIQUE_FIELD_IDS.includes(fieldId)) {
// Verifica se o valor está vazio
if (!userField.value || userField.value.trim() === "") {
return null; // Deixa a validação padrão lidar com valores vazios
}
const value = userField.value.trim();
// Verifica contra a lista de valores
const unique = isValueUnique(fieldId, value);
if (!unique) {
return {
failed: true,
ok: false,
reason: i18n("js.my_custom_plugin_text_validator.value_taken"),
element: userField.field.element,
};
}
}
return null;
});
});
Principais pontos a comentar aqui:
- UNIQUE_FIELD_IDS é usado para filtrar qual campo eu quero que seja afetado pela minha lógica personalizada. O número 5 é o ID do campo, ele pode ser verificado ao abrir a página Editar para o campo personalizado dentro da URL.
- isValueUnique é uma função personalizada onde toda a sua lógica personalizada faz a validação. Ela deve ser especificada fora de api.addCustomUserFieldValidationCallback.
Como precisamos verificar o valor no banco de dados, isso significa que precisamos fazer uma chamada de API para algum endpoint que fará a validação e nos fornecerá uma resposta se for válido ou não, OU pode haver outra maneira de verificar isso sem um endpoint personalizado que eu ainda não encontrei. Existe um módulo para alguma validação, mas sua validação é restrita a verificação REGEX diretamente no JS, o que não era o que eu precisava.
O arquivo plugin.rb é onde especificamos informações sobre nosso plugin personalizado, suas configurações e tudo o que pode ser adicionado. No meu caso, registro meu Endpoint personalizado para a validação. Aqui está um exemplo de validação possível, você pode ter de forma diferente.
# frozen_string_literal: true
# name: meu_plugin_personalizado
# about: Valida a exclusividade de campos personalizados de usuário
# version: 0.1
# authors: Yan R
enabled_site_setting :my_custom_plugin_enabled
after_initialize do
# Lista codificada de IDs de campos únicos
UNIQUE_FIELD_IDS = ["5"].freeze
# Adiciona endpoints de API para validação
Discourse::Application.routes.append do
get "/my_custom_plugin_endpoint_url/:field_id" => "my_custom_plugin_validation#validate"
end
class ::MyCustomPluginValidationController < ::ApplicationController
skip_before_action :verify_authenticity_token
skip_before_action :check_xhr
skip_before_action :redirect_to_login_if_required
def validate
field_name = params[:field_name]
field_value = params[:field_value]
return render json: { valid: true } unless field_name.present? && field_value.present?
return render json: { valid: true } unless field_name.start_with?("user_field_")
# Normaliza o valor: remove espaços em branco e compara sem distinção entre maiúsculas e minúsculas
normalized_value = field_value.to_s.strip
return render json: { valid: true } if normalized_value.empty?
existing = UserCustomField
.where(name: field_name)
.where("LOWER(TRIM(value)) = LOWER(?)", normalized_value)
.exists?
render json: { valid: !existing }
end
end
end
Agora, a parte complicada, você pode usar este endpoint e fazer uma chamada fetch/ajax do seu inicializador, mas não funcionará, o principal motivo é que addCustomUserFieldValidationCallback não funciona com callbacks assíncronos, então você precisa fazer uma chamada que não seja assíncrona. Usei XMLHttpRequest para isso, como exemplo xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true); onde true desabilita o modo assíncrono.
Isso começará a funcionar, mas você enfrentará um problema, onde não poderá digitar rapidamente no campo de entrada, porque addCustomUserFieldValidationCallback dispara com cada letra fornecida e esperará até ter um retorno adequado, então ele congela sua entrada.
Eu não mostrei nos exemplos, mas minha solução foi recuperar a lista de valores únicos do campo após o carregamento da página e usá-la para filtrar o valor diretamente no JS sem fazer chamadas de API adicionais, você só precisa garantir que tenha uma quantidade razoável de valores e proteger o endpoint. No meu caso, em vez da lista de valores, retornei uma lista de hashes, então no JS eu transformo o valor digitado em um hash e comparo os hashes. Além do fato de ser um pouco mais seguro, também reduz significativamente o peso da resposta, então você pode obter muito mais valores para comparar.
Aqui estão as informações sobre a pasta de configuração.
settings.yml:
plugins:
my_custom_plugin_enabled:
default: true
client: true
server.en.yml
en:
site_settings:
my_custom_plugin_enabled: "Habilitar validação de campos de usuário únicos"
my_custom_plugin_text_validator:
value_taken: "Este valor já foi utilizado"
client.en.yml
en:
js:
my_custom_plugin_text_validator:
value_taken: "Este valor já foi utilizado"
Adição: Não encontrei informações sobre como adicionar meu plugin personalizado além de adicioná-lo em app.yml, pois tenho uma configuração Docker. O que fiz foi adicionar meu plugin personalizado na pasta/repositório docker, que contém a configuração do Discourse e montá-lo durante a operação de Reconstrução na pasta de plugins dentro do Container.
Espero que isso ajude alguém a implementar validações personalizadas para os campos de usuário.