Cómo agregar campos personalizados a modelos

Esta es una colección de plugins educativos que demuestran cómo agregar un campo personalizado a diferentes modelos en Discourse. Están destinados como herramientas de aprendizaje para quienes deseen aprender a crear plugins de Discourse.

GitHub-Mark Cómo agregar un campo personalizado a un tema
GitHub-Mark Cómo agregar un campo personalizado a una categoría

Para quién están destinados

Estos plugins son para personas que desean aprender más sobre la creación de plugins de Discourse. Antes de comenzar a trabajar con estos plugins, debes completar la guía para principiantes sobre la creación de plugins de Discourse.

Podrías usar estos plugins simplemente para agregar campos personalizados en tu instancia de Discourse; sin embargo, aún necesitarías modificar ligeramente el código para hacerlo. No están diseñados para un uso plug-and-play en un servidor en vivo.

Cómo funcionan

Además de contener código funcional, cada plugin incluye una descripción paso a paso de lo que hace el código en forma de comentarios. Por ejemplo:

## 
# type:        step
# number:      1
# title:       Registrar el campo
# description: Aquí le indicamos a Discourse qué tipo de campo estamos agregando.
#              Puedes registrar un campo de tipo string, integer, boolean o 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 los pasos y los comentarios sean autoexplicativos. Las referencias están ahí para indicarte dónde buscar si deseas aprender más.

Házmelo saber si lo encuentras útil, si algo no funciona o si las notas no están claras :slight_smile:

28 Me gusta

Gracias por crear el plugin para nosotros.
Después de instalar el plugin, tengo este error:

¡Vaya!
El software que impulsa este foro de discusión encontró un problema inesperado. Lamentamos las molestias.
Se registró información detallada sobre el error y se generó una notificación automática. Lo revisaremos.
No es necesario realizar ninguna acción adicional. Sin embargo, si el error persiste, puedes proporcionar detalles adicionales, incluidos los pasos para reproducir el error, publicando un tema de discusión en la categoría de comentarios del sitio.

¡Hola, estás ejecutando esto en tu entorno de desarrollo local? Si es así, ¿podrías enviarme por mensaje privado los registros de desarrollo? Si tu entorno de desarrollo está configurado correctamente, este plugin funcionará.

1 me gusta

Lo leí, pero no logro entenderlo. ¿Podrías dedicar tiempo a crear un tutorial en video sobre cómo instalar este plugin, desde configurar un entorno de desarrollo local hasta agregar un campo personalizado paso a paso?
Es muy difícil acceder a este plugin.
Si es posible, ¿podrías mejorar la función de búsqueda para el tipo de campo numérico?

Puedo hacer una donación para este plugin; ¡por favor, ayúdame!

Este recurso que @angus ha preparado es aún más útil de lo que pensé inicialmente.

No solo está el código para agregar un campo personalizado con explicaciones claras, sino que gran parte de él se puede integrar directamente en cualquier plugin, ya que el código utiliza principalmente variables como FIELD_NAME y FIELD_VALUE, que puedes definir en plugin/config/settings.yml (también debes asegurarte de que la estructura de archivos de tu plugin sea la misma que la del código de GitHub que @angus ha proporcionado). Revisar el código también me ha permitido comprender mejor algunas funciones y métodos de Discourse que había visto antes, pero que no entendía realmente hasta ahora.

Hasta ahora, el código funciona perfectamente para crear y guardar campos personalizados de temas. Sin embargo, tengo dos preguntas que han surgido:

  1. Error en la lista de temas: Parece que lanza un error cuando intento cargar una lista de categorías (es decir, una lista de temas de una categoría) donde hay temas creados antes de agregar el campo personalizado. Muestra la página de excepción y muestra este error:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. ¿Cuál es la forma recomendada de solucionarlo?

  2. ¿Existe alguna manera de aplicar el campo personalizado solo a los temas de ciertas categorías? Por ejemplo, supongamos que tengo Categoría 1, Categoría 2 y Categoría 3, y solo quiero que aparezca el campo de entrada personalizado y que el campo se guarde solo si el tema pertenece a la Categoría 3. ¿Existe alguna forma de hacerlo?

1 me gusta

Nosotros (Pavilion) haremos algo así en el futuro, pero por ahora solo hay el código y los pasos. Si te atascas en un problema específico, crea una publicación en Dev y describe el problema con cierto detalle.

He añadido un subpaso al plugin de campos personalizados de temas que demuestra cómo precargar campos si los estás utilizando en la lista de temas.

Necesitarás un campo personalizado de categoría para identificar en cuáles categorías debe mostrarse. He aplicado el mismo tratamiento a los campos personalizados de categoría en este plugin, incluyendo las mismas instrucciones paso a paso:

Combinar estos dos plugins educativos no te llevará directamente a tu objetivo, pero ve si puedes llegar desde ahí.

2 Me gusta

¡Eso es fantástico, @angus! Muchas gracias.

Un video siempre es agradable; estoy totalmente a favor de hacer las cosas lo más sencillas posible, pero no creo que sea necesario para obtener el valor clave de estos recursos que @angus ha recopilado. Estos recursos proporcionan el código que necesitas para lograr el objetivo específico del recurso (tener un campo personalizado de tema o un campo personalizado de categoría funcionando). Un video probablemente solo consistiría en que @angus o alguien más explicara cómo implementar el recurso, pero eso es sencillo y probablemente podamos simplemente explicarlo aquí.

Para aclarar, estos recursos no son plugins que simplemente agregas a tu sitio como una solución lista para usar que personaliza tu foro. Más bien, te proporcionan de manera eficiente la comprensión necesaria para codificar tus propios campos personalizados en tu plugin.


Así es como he utilizado estos recursos:

Deberás agregar el nombre y el tipo de campo que deseas en config/settings. El código en estos recursos utiliza variables que se definen allí. Por lo tanto, en realidad no necesitas hacer mucha personalización del código para que funcione en tu propio plugin después de eso; las variables en plugin.rb y en otros lugares hacen referencia a config/settings y luego deberían funcionar.

Después de actualizar config/settings, puedes simplemente seguir el código y agregarlo a tu plugin:

  • Comienza con el código en plugin.rb y agrégalo al plugin.rb de tu propio plugin para crear el campo personalizado.

  • Luego, ve al inicializador (en assets/javascripts/discourse/[custom-field-initializer]) para obtener el código que inicializará el campo personalizado y te permitirá guardarlo en el servidor.

  • Luego, crea el formulario en la capa de vista que será donde el usuario (o tu aplicación, si la aplicación agrega el campo automáticamente) pueda ingresar el valor para el campo personalizado, aquí (assets/discourse/connectors/[plugin-outlet-name]/[tu plantilla especial].hbs).

  • @angus ha configurado esto para que agregues los formularios para los campos personalizados en un outlet de plugin que se insertará en la plantilla de Discourse. La configuración para este formulario está aquí (assets/javascripts/discourse/lib/[nombre-del-campo-personalizado].js.es6), por lo que probablemente también quieras personalizarla para que el formulario funcione.

@angus, siéntete libre de corregir cualquier cosa que haya dicho aquí.

Una vez que entendí cómo configurar el campo personalizado siguiendo los pasos anteriores, comencé a personalizar las cosas un poco más (por ejemplo, siendo más creativo con el funcionamiento del formulario), pero esto fue un punto de partida extremadamente útil que me ahorró horas de trabajo.

Después de revisarlo, tuve algunas preguntas (como pregunté antes), pero obtener respuestas en Dev parece ser la forma más útil de proceder a partir de ahí.

3 Me gusta

¡Gran descripción! Sí, así es como están pensados para usarse :+1:

1 me gusta

Edición: Originalmente publiqué mi pregunta sobre cómo recuperar elementos basados en un campo personalizado aquí, pero decidí que la pregunta era lo suficientemente diferente como para justificar su propia publicación. Así que la publiqué por separado aquí.

2 Me gusta

Estoy experimentando un comportamiento extraño con el compositor siguiendo el ejemplo de campos personalizados de tema.

Al hacer clic en el botón “crear tema” (por ejemplo, en la página de visualización de la categoría, pero en cualquier parte del sitio), el compositor no se abre y obtengo este error:

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

Empecé a ver este error cuando intenté agregar un nuevo botón “crear tema” en una nueva página, pero desde entonces, incluso si elimino ese nuevo botón y cualquier código relacionado, el error persiste.

De alguna manera, creo que el siguiente código, procedente del inicializador de campos personalizados de tema, está causando el problema:

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

Cuando elimino este código, los botones “crear tema” vuelven a funcionar correctamente (abriendo el compositor adecuadamente). Cuando vuelvo a poner el código, el error del compositor reaparece.

Tenía este código en mi plugin anteriormente sin problemas, pero ahora causa el error del compositor (incluso si he eliminado cualquier código relacionado con el compositor o el botón de crear tema en mi plugin).

Por supuesto, este código es importante, ya que serializa el campo personalizado, pero parece que está entrando en conflicto con el compositor. ¿Alguna idea sobre cómo solucionarlo?

Logré averiguar la causa: estaba intentando agregar dos campos personalizados distintos al inicializador de campos personalizados de temas. Por alguna razón, eso estaba causando interferencias. Probablemente existe una forma correcta de agregar dos campos personalizados a ese archivo, pero mi código, que simplemente repetía el mismo código para dos campos personalizados distintos, estaba generando problemas. Volvió a funcionar correctamente cuando eliminé el segundo campo personalizado de ese archivo.

Con este código esquelético, si quisiera añadir varios campos, ¿tendría que ser cada campo un plugin independiente?

Estoy muy emocionado de haber encontrado este tutorial y me pregunto, ¿cuánto, si es que necesito hacer algún ajuste a estas plantillas para que funcionen para campos de usuario personalizados?

No, solo necesitas agregar código adicional para el campo adicional. En la mayoría de los casos, simplemente duplicando el código existente, por ejemplo:

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

El primer lugar para buscar campos de usuario personalizados es /admin/customize/user_fields, que te proporciona una interfaz de usuario para agregarlos. Si deseas un control más granular, el proceso es muy similar al de temas y categorías, pero en realidad no necesitas los elementos del frontend con campos de usuario.

De hecho, nosotros (Pavilion) estamos pensando en crear un plugin de campos personalizados (análogo a ACF para WordPress) que inicialmente se parecería un poco a la interfaz de administración de campos personalizados en el plugin Custom Wizard.

De hecho, algunas personas ya utilizan el plugin Custom Wizard como un administrador de campos personalizados. Enumera todos los campos personalizados en tu instancia (de cualquier fuente) y te permite agregar un campo de cualquier tipo a cualquier modelo que los admita.

No agrega soporte de frontend, por ejemplo, como el que se muestra en el plugin educativo Topic Custom Field (y eso no funcionaría en el contexto del plugin Custom Wizard), por lo que estamos pensando en separar eso en un plugin independiente.

3 Me gusta

@angus, creo que me encantaría esto.

Especialmente si añade el soporte de frontend.

Me encantaría tener una forma fácil para que los administradores añadan campos personalizados a diferentes clases, permitan a los usuarios rellenarlos (por ejemplo, en temas, publicaciones, perfiles de usuario) y tengan una forma para que el frontend los muestre.

Lo principal que no consigo con los campos de usuario personalizados actuales es una variedad de tipos de campos. Ahora mismo está limitado a 4, creo, y me encantaría tener las opciones que están disponibles con el plugin Custom Wizards.

Idealmente, quiero construir un directorio de usuarios bastante avanzado, con capacidad de búsqueda, filtrado y ordenación, con muchos campos personalizados de muchos tipos. Experimentaré con Custom Wizards para ver si funcionará por ahora y espero que inviertan en el plugin Custom Fields.

¡Gracias!

@angus

En primer lugar, muchas gracias por este plugin.

¿Tienes posiblemente un ejemplo funcional de este plugin (campo personalizado a tema) para tener en cuenta varios campos personalizados? Pude agregar con éxito un campo personalizado e hice un par de modificaciones sin problemas.

He intentado duplicar el código, modificar y agregar un complemento adicional, etc.

¿Alguien tiene un repositorio de código o un ejemplo que esté dispuesto a compartir conmigo? Cualquier ayuda sería muy apreciada.

1 me gusta

Hola @Joe_Stanton,

Lo he hecho un par de veces y la forma en que lo he hecho ha sido almacenar los campos personalizados en un array con un objeto con un nombre y tipo para el campo personalizado.

Por ejemplo:

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

Luego recorro los campos para aplicar la lógica que se menciona en este tema. Puedes ver un ejemplo de su uso en un plugin en el que estoy trabajando aquí. Sin embargo, el código relevante está a continuación.

Ejemplo

En el lado del servidor:

  # Registro de campos personalizados
  fields.each do |field|
    # Registrar los 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

    # Actualizar en la creación del tema
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Actualizar en la edición del tema
    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

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

    # Pre-cargar los campos
    add_preloaded_topic_list_custom_field(field[:name])

    # Serializar a la lista de temas
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

De manera similar en el lado del cliente para serializar los campos:


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

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

Gracias @keegan!

Creo que configuré correctamente el archivo plugin.rb, el bucle parece correcto.

Sin embargo, estoy teniendo problemas con el archivo topic-custom-field-initializer.js. Aquí está mi código para ambos archivos. ¿Alguna sugerencia para el archivo initializer.js? Creo que estoy muy cerca, al crear un nuevo tema, obtengo 1/3 de los campos, el campo listingDetails, pero todavía me faltan isClassifiedListing y 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 me gusta

No lo he probado, pero creo que la razón por la que solo ves 1/3 de los campos es que está iterando y registrando una clase de conector no única y sobrescribiendo la anterior.

En general, para el lado del cliente, en lugar de iterar a través de los campos personalizados y declarar los métodos de la API. Sugiero que definas componentes para cada campo por separado, o al menos acciones separadas, ya que probablemente necesitarás tener una lógica diferente asociada con cada campo.

La única parte sobre la que iteraría y declararía es esta:

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

El resto de los componentes, probablemente sea mejor crear una lógica separada para cada caso.

3 Me gusta

@keegan ¡eso funcionó! muchas gracias por todas las ideas. No podría haberlo hecho sin tu ayuda.

4 Me gusta