Como adicionar campos personalizados aos modelos

Esta é uma coleção de plugins educacionais que demonstram como adicionar um campo personalizado a diferentes modelos no Discourse. Eles são destinados como ferramentas de aprendizado para quem deseja aprender a criar plugins para o Discourse.

GitHub-Mark Como adicionar um campo personalizado a um tópico
GitHub-Mark Como adicionar um campo personalizado a uma categoria

Para quem são destinados

Esses plugins são para pessoas que desejam aprender mais sobre a criação de plugins para o Discourse. Antes de começar a trabalhar com esses plugins, você deve concluir o guia para iniciantes sobre como criar plugins para o Discourse.

Você pode usar esses plugins apenas para adicionar campos personalizados na sua instância do Discourse, mas ainda precisaria modificar ligeiramente o código para fazer isso. Eles não foram projetados para uso imediato em um servidor em produção.

Como funcionam

Além de conterem código funcional, cada plugin inclui uma descrição passo a passo do que o código está fazendo, na forma de comentários. Por exemplo:

## 
# type:        step
# number:      1
# title:       Register the field
# description: Onde informamos ao Discourse qual tipo de campo estamos adicionando.
#              Você pode registrar um campo do tipo string, integer, boolean ou json.
# references:  lib/plugins/instance.rb,
#              app/models/concerns/has_custom_fields.rb
##
register_topic_custom_field_type(FIELD_NAME, FIELD_TYPE.to_sym)

Esperamos que as etapas e os comentários sejam autoexplicativos. Os references estão lá para indicar onde procurar caso deseje aprender mais.

Fique à vontade para me avisar se achar útil, se algo não estiver funcionando ou se as notas não estiverem claras :slight_smile:

28 curtidas

Obrigado por criar o plugin para nós.
Tenho este erro após instalar o plugin:

Oops
O software que alimenta este fórum de discussões encontrou um problema inesperado. Pedimos desculpas pelo inconveniente.
Informações detalhadas sobre o erro foram registradas e uma notificação automática foi gerada. Vamos analisar.
Nenhuma ação adicional é necessária. No entanto, se o erro persistir, você pode fornecer mais detalhes, incluindo os passos para reproduzir o erro, postando um tópico na categoria de feedback do site.

Ei, você está rodando isso no seu ambiente de desenvolvimento local? Se sim, poderia enviar por MP seus logs de desenvolvimento? Se seu ambiente de dev estiver configurado corretamente, esse plugin funcionará.

1 curtida

Li, mas não consegui entender. Você poderia dedicar um tempo para criar um tutorial em vídeo mostrando como instalar este plugin, desde a configuração de um ambiente de desenvolvimento local até a adição passo a passo de um campo personalizado? … É muito difícil acessar este plugin. Se possível, você poderia melhorar a funcionalidade de busca para o tipo de campo numérico?

Posso doar para este plugin; por favor, me ajude com o suporte!

Este recurso que @angus preparou é ainda mais útil do que eu percebi inicialmente.

Não apenas o código para adicionar um campo personalizado está lá com explicações claras, mas grande parte dele pode ser integrada diretamente em qualquer plugin, pois o código usa principalmente variáveis como FIELD_NAME e FIELD_VALUE, que você pode definir em plugin/config/settings.yml (você também precisa garantir que a estrutura de arquivos do seu plugin seja a mesma do código no GitHub fornecido por @angus). Analisar o código também me deu uma compreensão maior de algumas funções e métodos do Discourse que eu já tinha visto antes, mas nunca realmente entendi até agora.

Até agora, o código funciona muito bem para criar e salvar campos personalizados de tópicos. Tenho duas perguntas que têm surgido:

  1. Erro na Lista de Tópicos: Parece gerar um erro no caso em que tento carregar uma lista de categorias (ou seja, uma lista de tópicos de uma categoria) onde existem tópicos criados antes da adição do campo personalizado. A página de exceção é exibida, listando o seguinte erro:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. Qual é a maneira recomendada de resolver isso?

  2. Existe uma maneira de aplicar o campo personalizado apenas para tópicos em certas categorias? Então, digamos que eu tenha a Categoria 1, Categoria 2 e Categoria 3, e eu queira que a entrada do campo personalizado apareça e que o campo seja salvo apenas se o tópico fizer parte da Categoria 3. Existe uma maneira de fazer isso?

1 curtida

Nós (Pavilion) faremos algo assim no futuro, mas por enquanto, temos apenas o código e os passos. Se você estiver travado em um problema específico, crie um post em Development e descreva o problema com algum detalhe.

Adicionei um subpasso ao plugin de campos personalizados de tópico demonstrando como pré-carregar campos se você estiver os utilizando na lista de tópicos.

Você precisará de um campo personalizado de categoria para identificar as categorias em que ele deve ser exibido. Apliquei o mesmo tratamento aos campos personalizados de categoria neste plugin, incluindo instruções passo a passo idênticas:

Combinar esses dois plugins educacionais não levará você totalmente ao seu objetivo, mas veja se consegue avançar a partir daqui.

2 curtidas

Isso é fantástico, @angus. Muito obrigado.

Um vídeo é sempre bom — sou totalmente a favor de tornar as coisas o mais diretas possível, mas não acho que seja necessário para obter o valor principal desses recursos que @angus reuniu. Esses recursos fornecem o código necessário para atingir o objetivo específico do recurso (ter um campo personalizado de tópico funcionando ou um campo personalizado de categoria). Um vídeo provavelmente seria apenas @angus ou outra pessoa explicando como implementar o recurso, mas isso é direto, e podemos provavelmente apenas descrevê-lo aqui.

Para ficar claro, esses recursos não são plugins que você apenas adiciona ao seu site como plug-and-play para personalizar seu fórum. Em vez disso, eles fornecem de forma eficiente o entendimento necessário para codificar seus próprios campos personalizados em seu plugin.


É assim que usei esses recursos:

Você precisará adicionar o nome e o tipo do campo desejado em config/settings. O código nesses recursos usa variáveis definidas ali. Então, na verdade, você não precisa fazer muita personalização do código para fazê-lo funcionar em seu próprio plugin depois disso — as variáveis em plugin.rb e em outros lugares referem-se a config/settings e, em seguida, devem funcionar.

Depois de atualizar config/settings, você pode simplesmente seguir o código, adicionando-o ao seu plugin:

  • Comece com o código em plugin.rb e adicione-o ao plugin.rb do seu próprio plugin para criar o campo personalizado.

  • Em seguida, vá até o initializer (em assets/javascripts/discourse/[custom-field-initializer]) para obter o código que inicializará o campo personalizado e permitirá salvá-lo no servidor.

  • Depois, crie o formulário na camada de visualização, que será onde o usuário (ou seu aplicativo, se o aplicativo adicionar o campo automaticamente) poderá inserir o valor do campo personalizado, aqui (assets/discourse/connectors/[plugin-outlet-name]/[seu-template-especial].hbs).

  • @angus configurou isso para que você adicionasse os formulários para os campos personalizados em uma saída de plugin que será inserida no template do Discourse. As configurações para esse formulário estão aqui (assets/javascripts/discourse/lib/[nome-do-campo-personalizado].js.es6), então você provavelmente também quererá personalizá-lo para fazer o formulário funcionar.

@angus, sinta-se à vontade para corrigir qualquer coisa que eu tenha dito aqui.

Depois que peguei o jeito de configurar o campo personalizado seguindo os passos acima, comecei a personalizar um pouco mais as coisas (por exemplo, sendo mais criativo com o funcionamento do formulário), mas isso foi um ponto de partida extremamente útil que me economizou horas de trabalho.

Depois de passar por tudo, tive algumas perguntas (como perguntei anteriormente), mas obter respostas em Development parece ser a maneira mais útil de proceder a partir daí.

3 curtidas

Ótima descrição! Sim, é assim que eles são destinados a ser usados :+1:

1 curtida

Edição: Originalmente, postei minha pergunta sobre como recuperar itens com base em um campo personalizado aqui, mas decidi que a pergunta era suficientemente diferente para justificar sua própria postagem. Portanto, publiquei separadamente aqui.

2 curtidas

Estou enfrentando um comportamento estranho com o composer seguindo o exemplo de campos personalizados de tópico.

Quando clico no botão “criar tópico” (por exemplo, na página de exibição da categoria—mas em qualquer lugar do site), o composer falha ao abrir e recebo este erro:

Uncaught Error: Assertion Failed: The key provided to set must be a string or number, you passed undefined
    at assert (index.js:172)
    at set (index.js:2802)
    at Class.set (observable.js:176)
    at composer.js:769
    at Array.forEach (<anonymous>)
    at Class.open (composer.js:768)
    at composer.js:898
    at invokeCallback (rsvp.js:493)
    at rsvp.js:558
    at rsvp.js:19
```\nComecei a ver esse erro quando tentei adicionar um novo botão "criar tópico" em uma nova página, mas desde então, mesmo removendo esse novo botão e mesmo removendo qualquer código relacionado, o erro persiste.

De certa forma, acho que o seguinte código—do topic-custom-field-initializer—está causando o problema:

api.serializeOnCreate(fieldName);
api.serializeToDraft(fieldName);
api.serializeToTopic(fieldName, topic.${fieldName});

Quando removo esse código, os botões de criar tópico voltam a funcionar (abrindo corretamente o composer). Quando coloco o código de volta, o erro do composer retorna.

Eu tinha esse código no meu plugin anteriormente sem problemas. Mas agora ele causa o erro do composer (mesmo que eu tenha removido qualquer código relacionado ao composer ou ao botão de criar tópico no meu plugin).

Claro, esse código é importante—ele serializa o campo personalizado. Mas parece que está conflitando com o composer. Alguma ideia de como corrigir?

Consegui descobrir a causa: eu estava tentando adicionar dois campos personalizados distintos ao inicializador de campos personalizados de tópico. Por algum motivo, isso estava causando interferência. Provavelmente existe uma maneira adequada de adicionar dois campos personalizados a esse arquivo, mas meu código, que repetia o mesmo trecho para os dois campos, estava gerando problemas. Tudo voltou a funcionar corretamente quando removi o segundo campo personalizado desse arquivo.

Com este código esquelético, se eu quisesse adicionar vários campos, cada campo teria que ser um plugin independente?

Estou muito animado por ter encontrado este tutorial e estou me perguntando, quanta alteração, se houver, eu precisaria fazer nesses modelos para que eles funcionassem para campos de usuário personalizados?

Não, você só precisa adicionar código adicional para o campo adicional. Na maioria dos casos, basta duplicar o código existente, por exemplo:

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

O primeiro lugar para procurar campos de usuário personalizados é em /admin/customize/user_fields, que fornece uma interface de usuário para adicioná-los. Se você quiser ter um controle mais granular, o processo é muito semelhante ao de tópicos e categorias, mas você não precisa realmente dos elementos de front-end com campos de usuário.

Na verdade, nós (Pavilion) estamos pensando em criar um plugin de campos personalizados (análogo ao ACF para WordPress) que inicialmente se pareceria um pouco com a interface de administração de campos personalizados no Plugin Custom Wizard.

Na verdade, algumas pessoas já usam o plugin Custom Wizard como um gerenciador de campos personalizados. Ele lista todos os campos personalizados em sua instância (de qualquer fonte) e permite que você adicione um campo de qualquer tipo a qualquer modelo que os suporte.

Ele não adiciona suporte de front-end, por exemplo, como o mostrado no plugin educacional Topic Custom Field (e isso não funcionaria no contexto do plugin Custom Wizard), que é por isso que estamos pensando em separá-lo em um plugin separado.

3 curtidas

@angus, acho que adoraria isso.

Especialmente se ele adicionar o suporte no front-end.

Eu adoraria ter uma maneira fácil para os administradores adicionarem campos personalizados a diferentes classes, permitir que os usuários os preencham (por exemplo, em tópicos, posts, perfis de usuário) e ter uma maneira para o front-end exibi-los.

A principal coisa que não consigo com os campos de usuário personalizados atuais é uma variedade de tipos de campo. Atualmente, é limitado a 4, acho, e eu adoraria ter as opções disponíveis com o plugin Custom Wizards.

Idealmente, quero construir um diretório de usuários bastante avançado, pesquisável/filtrável/ordenável, com muitos campos personalizados de vários tipos. Experimentarei o Custom Wizards para ver se funcionará por enquanto e espero que vocês invistam no plugin Custom Fields.

Obrigado!

@angus

Primeiramente, muito obrigado por este plugin.

Você teria um exemplo funcional deste plugin (campo personalizado para tópico) para acomodar vários campos personalizados? Consegui adicionar com sucesso um campo personalizado e fiz algumas modificações sem problemas.

Já tentei duplicar o código, modificar e adicionar um plugin adicional, etc.

Alguém tem um repositório de código ou exemplo que esteja disposto a compartilhar comigo? Qualquer ajuda seria muito apreciada.

1 curtida

Olá @Joe_Stanton,

Eu já fiz isso algumas vezes e a maneira que eu fiz foi armazenar os campos personalizados em um array com um objeto com um nome e tipo para o campo personalizado.

Por exemplo:

fields = [
  { name: 'isClassifiedListing', type: 'boolean' },
  { name: 'listingStatus', type: 'string' },
  { name: "listingDetails", type: 'json' }
]

Depois eu itero pelos campos para aplicar a lógica mencionada neste tópico. Você pode ver um exemplo de como isso é usado em um plugin que estou desenvolvendo aqui. No entanto, o código relevante está abaixo.

Exemplo

No lado do servidor:

  # Registro de Campos Personalizados
  fields.each do |field|
    # Registra os campos
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

    # Métodos Getter
    add_to_class(:topic, field[:name].to_sym) do
      if !custom_fields[field[:name]].nil?
        custom_fields[field[:name]]
      else
        nil
      end
    end

    # Métodos Setter
    add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
    end

    # Atualização na Criação do Tópico
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Atualização na Edição do Tópico
    PostRevisor.track_topic_field(field[:name].to_sym) do |tc, value|
      tc.record_change(field[:name], tc.topic.send(field[:name]), value)
      tc.topic.send("#{field[:name]}=".to_sym, value.present? ? value : nil)
    end

    # Serializa para o Tópico
    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

    # Pré-carrega os Campos
    add_preloaded_topic_list_custom_field(field[:name])

    # Serializa para a lista de tópicos
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

Similarmente no lado do cliente para serializar os campos:


 const CUSTOM_FIELDS = [
  { name: "isClassifiedListing", type: "boolean" },
  { name: "listingStatus", type: "string" },
  { name: "listingDetails", type: "json" },
];

  // Serializa Campos Personalizados:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 curtidas

Obrigado @keegan!

Acho que configurei o arquivo plugin.rb corretamente, o loop parece correto.

No entanto, estou com dificuldades no arquivo topic-custom-field-initializer.js. Aqui está meu código para ambos os arquivos. Alguma dica para o arquivo initializer.js? Acho que estou muito perto aqui, ao criar um novo tópico, estou recebendo 1/3 dos campos, o campo listingDetails, mas ainda faltam isClassifiedListing e listingStatus.

enabled_site_setting :topic_custom_field_enabled
register_asset 'stylesheets/common.scss'

after_initialize do
  fields = [
  { name: 'isClassifiedListing', type: 'boolean' },
  { name: 'listingStatus', type: 'string' },
  { name: "listingDetails", type: 'json' }
]

 fields.each do |field|

  register_topic_custom_field_type(field[:name], field[:type].to_sym)

   add_to_class(:topic, field[:name].to_sym) do
      if !custom_fields[field[:name]].nil?
        custom_fields[field[:name]]
      else
        nil
      end
    end

   add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
   end

   on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
   end

    PostRevisor.track_topic_field(field[:name].to_sym) do |tc, value|
      tc.record_change(field[:name], tc.topic.send(field[:name]), value)
      tc.topic.send("#{field[:name]}=".to_sym, value.present? ? value : nil)
    end

    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

  add_preloaded_topic_list_custom_field(field[:name])

    # Serialize to the topic list
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end

end

end

Initializer.js

import { withPluginApi } from 'discourse/lib/plugin-api';
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from '@ember/object/computed';
import { isDefined, fieldInputTypes } from '../lib/topic-custom-field';

export default {
  name: "topic-custom-field-intializer",
  initialize(container) {


    const CUSTOM_FIELDS = [
      { name: "isClassifiedListing", type: "boolean" },
      { name: "listingStatus", type: "string" },
      { name: "listingDetails", type: "json" },
    ];

    CUSTOM_FIELDS.forEach((field) => {

    withPluginApi('0.11.2', api => {

      api.registerConnectorClass('composer-fields', 'composer-topic-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;

          if (!isDefined(model[field.name]) && model.topic && model.topic[field.name]) {
            model.set(field.name, model.topic[field.name]);
          }

          let props = {
            fieldName: field.name,
            fieldValue: model.get(field.name)
          }
          component.setProperties(Object.assign(props, fieldInputTypes(field.type)));
        },

        actions: {
          onChangeField(fieldValue) {
            this.set(`model.${field.name}`, fieldValue);
          }
        }
      });

      api.registerConnectorClass('edit-topic', 'edit-topic-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;

          let props = {
            fieldName: field.name,
            fieldValue: model.get(field.name)
          }
          component.setProperties(Object.assign(props, fieldInputTypes(field.type)));
        },

        actions: {
          onChangeField(fieldValue) {
            this.set(`buffered.${field.name}`, fieldValue);
          }
        }
      });

      api.serializeOnCreate(field.name);
      api.serializeToDraft(field.name);
      api.serializeToTopic(field.name, `topic.${field.name}`);

      api.registerConnectorClass('topic-title', 'topic-title-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;
          const controller = container.lookup('controller:topic');

          component.setProperties({
            fieldName: field.name,
            fieldValue: model.get(field.name),
            showField: !controller.get('editingTopic') && isDefined(model.get(field.name))
          });

          controller.addObserver('editingTopic', () => {
            if (this._state === 'destroying') return;
            component.set('showField', !controller.get('editingTopic') && isDefined(model.get(field.name)));
          });

          model.addObserver(field.name, () => {
            if (this._state === 'destroying') return;
            component.set('fieldValue', model.get(field.name));
          });
        }
      });

      api.modifyClass('component:topic-list-item', {
        customFieldName: field.name,
        customFieldValue: alias(`topic.${field.name}`),

        @discourseComputed('customFieldValue')
        showCustomField: (value) => (isDefined(value))
      });

    });


    });
  }
}
1 curtida

Ainda não testei, mas acredito que o motivo de você estar vendo apenas 1/3 dos campos é que ele está iterando e registrando uma classe de conector não exclusiva, substituindo a anterior.

Em geral, para o lado do cliente, em vez de iterar sobre os campos personalizados e declarar os métodos da API, sugiro que você defina componentes para cada campo separadamente, ou pelo menos ações separadas, pois provavelmente você precisará ter lógica diferente associada a cada campo.

A única parte sobre a qual eu iteraria e declararia é esta:

  api.serializeOnCreate(field.name);
      api.serializeToDraft(field.name);
      api.serializeToTopic(field.name, `topic.${field.name}`);

Para o restante dos componentes, provavelmente é melhor criar lógica separada para cada caso.

3 curtidas

@keegan funcionou! muito obrigado por todos os insights. Não teria conseguido sem sua ajuda.

4 curtidas