Comment ajouter des champs personnalisés aux modèles

Voici une collection de plugins éducatifs qui démontrent comment ajouter un champ personnalisé à différents modèles dans Discourse. Ils sont conçus comme des outils d’apprentissage pour ceux qui souhaitent apprendre à créer des plugins Discourse.

GitHub-Mark Comment ajouter un champ personnalisé à un sujet
GitHub-Mark Comment ajouter un champ personnalisé à une catégorie

À qui s’adressent-ils ?

Ces plugins s’adressent aux personnes souhaitant en savoir plus sur la création de plugins Discourse. Avant de commencer à travailler avec ces plugins, vous devez consulter le guide pour débutants sur la création de plugins Discourse.

Vous pourriez utiliser ces plugins simplement pour ajouter des champs personnalisés sur votre instance Discourse, mais vous devriez tout de même modifier légèrement le code pour ce faire. Ils ne sont pas conçus pour un usage « plug-and-play » sur un serveur en production.

Comment ils fonctionnent

En plus de contenir du code fonctionnel, chaque plugin inclut une description étape par étape de ce que fait le code, sous forme de commentaires. Par exemple :

## 
# type:        step
# number:      1
# title:       Register the field
# description: Where we tell discourse what kind of field we're adding.
#              You can register a string, integer, boolean or json field.
# references:  lib/plugins/instance.rb,
#              app/models/concerns/has_custom_fields.rb
##
register_topic_custom_field_type(FIELD_NAME, FIELD_TYPE.to_sym)

J’espère que les étapes et les commentaires sont clairs par eux-mêmes. Les references sont là pour vous indiquer où regarder si vous souhaitez en savoir plus.

Faites-moi savoir si vous trouvez cela utile, si quelque chose ne fonctionne pas, ou si les notes ne sont pas claires :slight_smile:

28 « J'aime »

Merci d’avoir créé ce plugin pour nous.
J’ai rencontré cette erreur après l’installation du plugin :

Oups
Le logiciel alimentant ce forum de discussion a rencontré un problème inattendu. Nous nous excusons pour la gêne occasionnée.
Des informations détaillées sur l’erreur ont été consignées et une notification automatique a été générée. Nous allons l’examiner.
Aucune action supplémentaire n’est nécessaire. Cependant, si le problème persiste, vous pouvez fournir des détails supplémentaires, y compris les étapes permettant de reproduire l’erreur, en créant un sujet de discussion dans la catégorie « Commentaires » du site.

Salut, tu l’exécutes dans ton environnement de développement local ? Si oui, pourrais-tu m’envoyer tes journaux de développement en message privé ? Si ton environnement de développement est correctement configuré, ce plugin fonctionnera.

1 « J'aime »

Je lis, mais je n’arrive pas à comprendre. Pouvez-vous prendre le temps de créer un tutoriel vidéo pour installer ce plugin, depuis la mise en place d’un environnement de développement local jusqu’à l’ajout pas à pas d’un champ personnalisé ?
C’est trop difficile d’accéder à ce plugin.
Si possible, pourriez-vous améliorer la fonction de recherche pour le type de champ « nombre » ?

Je suis prêt à faire un don pour ce plugin ; veuillez m’aider, s’il vous plaît !

Cette ressource que @angus a préparée s’avère encore plus utile que je ne le pensais initialement.

Non seulement le code pour ajouter un champ personnalisé y figure avec des explications claires, mais une grande partie peut être intégrée directement dans n’importe quel plugin, car le code utilise principalement des variables comme FIELD_NAME et FIELD_VALUE, que vous pouvez définir dans plugin/config/settings.yml (vous devez également vous assurer que la structure de fichiers de votre plugin correspond à celle du code GitHub fourni par @angus). L’analyse de ce code m’a également permis de mieux comprendre certaines fonctions et méthodes de Discourse que j’avais déjà rencontrées, mais que je n’avais jamais vraiment comprises jusqu’à présent.

Jusqu’à présent, le code fonctionne parfaitement pour créer et enregistrer des champs personnalisés de sujets. Deux questions se sont posées à moi :

  1. Erreur dans la liste des sujets : Il semble qu’une erreur se produise lorsque j’essaie de charger une liste de catégories (c’est-à-dire une liste de sujets pour une catégorie) contenant des sujets créés avant l’ajout du champ personnalisé. La page d’exception s’affiche, indiquant l’erreur suivante :
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. Quelle est la méthode recommandée pour résoudre ce problème ?

  2. Existe-t-il un moyen d’appliquer le champ personnalisé uniquement aux sujets de certaines catégories ? Par exemple, si j’ai la Catégorie 1, la Catégorie 2 et la Catégorie 3, et que je souhaite que le champ personnalisé ne s’affiche et ne soit enregistré que si le sujet appartient à la Catégorie 3. Est-il possible de faire cela ?

1 « J'aime »

Nous (Pavilion) prévoyons de faire quelque chose de similaire à l’avenir, mais pour l’instant, seules le code et les étapes sont disponibles. Si vous êtes bloqué sur un problème spécifique, créez un post dans Dev et décrivez le problème en détail.

J’ai ajouté une sous-étape au plugin de champs personnalisés des sujets pour montrer comment précharger les champs si vous les utilisez dans la liste des sujets.

Vous aurez besoin d’un champ personnalisé de catégorie pour identifier les catégories dans lesquelles il doit être affiché. J’ai appliqué le même traitement aux champs personnalisés de catégories dans ce plugin, avec les mêmes instructions étape par étape :

La combinaison de ces deux plugins éducatifs ne vous mènera pas entièrement à votre objectif, mais voyez si vous pouvez y parvenir à partir de là.

2 « J'aime »

C’est fantastique, @angus. Merci beaucoup.

Une vidéo est toujours appréciée — je suis tout à fait favorable à la simplicité, mais je ne pense pas qu’elle soit nécessaire pour obtenir la valeur clé de ces ressources que @angus a réunies. Ces ressources fournissent le code nécessaire pour atteindre l’objectif spécifique de chaque ressource (avoir un champ personnalisé de sujet fonctionnel ou un champ personnalisé de catégorie). Une vidéo consisterait probablement simplement à ce que @angus ou quelqu’un d’autre explique comment implémenter la ressource, mais cela est simple, et nous pouvons probablement simplement l’exposer ici.

Pour être clair, ces ressources ne sont pas des plugins que vous ajoutez simplement à votre site comme un plug-and-play pour personnaliser votre forum. Elles vous donnent plutôt efficacement la compréhension nécessaire pour coder vos propres champs personnalisés dans votre plugin.


Voici comment j’ai utilisé ces ressources :

Vous devrez ajouter le nom et le type de champ souhaité dans config/settings. Le code de ces ressources utilise des variables définies là-bas. Ainsi, vous n’avez en réalité pas besoin de beaucoup personnaliser le code pour qu’il fonctionne dans votre propre plugin après cela — les variables dans plugin.rb et ailleurs font référence à config/settings, et devraient ensuite fonctionner.

Après avoir mis à jour config/settings, vous pouvez simplement suivre le code et l’ajouter à votre plugin :

  • Commencez par le code dans plugin.rb et ajoutez-le au fichier plugin.rb de votre propre plugin afin de créer le champ personnalisé.

  • Ensuite, rendez-vous dans le initialiseur (dans assets/javascripts/discourse/[custom-field-initializer]) pour obtenir le code qui initialisera le champ personnalisé et vous permettra de l’enregistrer sur le serveur.

  • Créez ensuite le formulaire dans la couche de vue où l’utilisateur (ou votre application, si l’application ajoute le champ automatiquement) pourra entrer la valeur du champ personnalisé, ici (assets/discourse/connectors/[plugin-outlet-name]/[votre modèle spécial].hbs).

  • @angus a configuré ces éléments de sorte que vous deviez ajouter les formulaires pour les champs personnalisés dans un plugin outlet qui sera inséré dans le modèle de Discourse. Les paramètres pour ce formulaire se trouvent ici (assets/javascripts/discourse/lib/[nom-du-champ-personnalisé].js.es6), vous voudrez probablement aussi personnaliser cela pour que le formulaire fonctionne.

@angus, n’hésitez pas à corriger tout ce que j’ai dit ici.

Une fois que j’ai compris comment configurer le champ personnalisé en suivant les étapes ci-dessus, j’ai commencé à personnaliser un peu plus les choses (par exemple, en étant plus créatif sur le fonctionnement du formulaire), mais c’était un point de départ extrêmement utile qui m’a fait gagner des heures de travail.

Après avoir tout parcouru, j’ai eu quelques questions (comme je l’ai demandé plus tôt), mais obtenir des réponses dans Dev semble être la méthode la plus utile pour avancer à partir de là.

3 « J'aime »

Belle description ! Oui, c’est ainsi qu’elles sont censées être utilisées :+1:

1 « J'aime »

Édition : J’avais initialement posté ma question sur la manière de récupérer des éléments en fonction d’un champ personnalisé ici, mais j’ai décidé que la question était suffisamment différente pour mériter son propre post. J’ai donc publié séparément ici.

2 « J'aime »

Je rencontre un comportement étrange avec l’éditeur de texte suite à l’exemple des champs personnalisés pour les sujets.

Lorsque je clique sur le bouton « Créer un sujet » (par exemple, sur la page d’affichage d’une catégorie, mais cela se produit partout sur le site), l’éditeur ne s’ouvre pas et j’obtiens cette erreur :

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

J’ai commencé à voir cette erreur lorsque j’ai essayé d’ajouter un nouveau bouton « Créer un sujet » sur une nouvelle page. Depuis, même si je supprime ce nouveau bouton et même si j’enlève tout le code associé, l’erreur persiste.

D’une certaine manière, je pense que le code suivant, issu de l’initialiseur des champs personnalisés pour les sujets, est à l’origine du problème :

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

Lorsque je supprime ce code, les boutons « Créer un sujet » fonctionnent à nouveau (l’éditeur s’ouvre correctement). Dès que je remets le code, l’erreur de l’éditeur réapparaît.

J’avais déjà ce code dans mon plugin sans problème. Mais maintenant, il provoque cette erreur (même si j’ai supprimé tout code lié à l’éditeur ou aux boutons de création de sujet dans mon plugin).

Bien sûr, ce code est important car il assure la sérialisation du champ personnalisé. Cependant, il semble entrer en conflit avec l’éditeur. Avez-vous des idées pour résoudre ce problème ?

J’ai réussi à identifier la cause : je tentais d’ajouter deux champs personnalisés distincts à l’initialiseur des champs personnalisés des sujets. Pour une raison quelconque, cela provoquait des interférences. Il existe probablement un moyen approprié d’ajouter deux champs personnalisés dans ce fichier, mais mon code, qui répétait simplement le même bloc pour les deux champs distincts, créait des problèmes. Tout a fonctionné à nouveau correctement lorsque j’ai retiré le deuxième champ personnalisé de ce fichier.

Avec ce code squelette, si je voulais ajouter plusieurs champs, chaque champ devrait-il être un plugin autonome ?

Je suis vraiment ravi d’avoir trouvé ce tutoriel et je me demande, combien, le cas échéant, de modifications devrais-je apporter à ces modèles pour qu’ils fonctionnent pour des champs d’utilisateur personnalisés ?

Non, il vous suffit d’ajouter du code supplémentaire pour le champ supplémentaire. Dans la plupart des cas, il suffit de dupliquer le code existant, par exemple :

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

Le premier endroit où chercher les champs personnalisés des utilisateurs est /admin/customize/user_fields, qui vous donne une interface utilisateur pour les ajouter. Si vous souhaitez un contrôle plus granulaire, le processus ressemble beaucoup à celui des sujets et des catégories, mais vous n’avez pas réellement besoin des éléments frontaux avec les champs utilisateurs.

En fait, nous (Pavilion) envisageons de créer un plugin de champs personnalisés (similaire à ACF pour WordPress) qui ressemblerait initialement un peu à l’interface d’administration des champs personnalisés du plugin Custom Wizard.

En fait, certaines personnes utilisent déjà le plugin Custom Wizard comme gestionnaire de champs personnalisés. Il répertorie tous les champs personnalisés de votre instance (de n’importe quelle source) et vous permet d’ajouter un champ de n’importe quel type à n’importe quel modèle qui les prend en charge.

Il n’ajoute pas de prise en charge frontale, par exemple comme celle montrée dans le plugin éducatif Topic Custom Field (et cela ne fonctionnerait pas dans le contexte du plugin Custom Wizard), c’est pourquoi nous envisageons de le séparer dans un plugin distinct.

3 « J'aime »

@angus, je pense que j’adorerais ça.

Surtout s’il ajoute le support côté frontend.

J’adorerais avoir un moyen facile pour les administrateurs d’ajouter des champs personnalisés à différentes classes, de permettre aux utilisateurs de les remplir (par exemple, pour les sujets, les messages, les profils d’utilisateurs), et d’avoir un moyen pour le frontend de les afficher.

La principale chose que je n’arrive pas à obtenir avec les champs d’utilisateurs personnalisés actuels est une variété de types de champs. Actuellement, il est limité à 4 je pense, et j’aimerais avoir les options disponibles avec le plugin Custom Wizards.

Idéalement, je veux construire un répertoire d’utilisateurs assez avancé, consultable/filtrable/triable, avec de nombreux champs personnalisés de nombreux types. J’expérimenterai avec Custom Wizards pour voir si cela fonctionnera pour l’instant et j’espère que vous investirez dans le plugin Custom Fields.

Merci !

@angus

Tout d’abord, merci beaucoup pour ce plugin.

Auriez-vous par hasard un exemple fonctionnel de ce plugin (champ personnalisé vers sujet) pour tenir compte de plusieurs champs personnalisés ? J’ai réussi à ajouter un champ personnalisé et à apporter quelques modifications sans problème.

J’ai essayé de dupliquer le code, de modifier et d’ajouter un plugin supplémentaire, etc.

Quelqu’un a-t-il un dépôt de code ou un exemple qu’il est prêt à partager avec moi ? Toute aide serait grandement appréciée.

1 « J'aime »

Salut @Joe_Stanton,

Je l’ai fait plusieurs fois, et la façon dont je l’ai fait a été de stocker les champs personnalisés dans un tableau avec un objet contenant un nom et un type pour le champ personnalisé.

Par exemple :

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

Ensuite, je parcours les champs pour appliquer la logique mentionnée dans ce sujet. Vous pouvez voir un exemple de son utilisation dans un plugin sur lequel je travaille ici. Cependant, le code pertinent est ci-dessous.

Exemple

Côté serveur :

  # Enregistrement des champs personnalisés
  fields.each do |field|
    # Enregistre les champs
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

    # Méthodes 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éthodes Setter
    add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
    end

    # Mise à jour à la création du sujet
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Mise à jour lors de la modification du sujet
    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

    # Sérialisation vers le sujet
    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

    # Préchargement des champs
    add_preloaded_topic_list_custom_field(field[:name])

    # Sérialisation vers la liste des sujets
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

De même côté client pour sérialiser les champs :


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

  // Sérialisation des champs personnalisés :
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 « J'aime »

Merci @keegan !

Je pense avoir correctement configuré le fichier plugin.rb, la boucle semble correcte.

Cependant, j’ai des difficultés avec le fichier topic-custom-field-initializer.js. Voici mon code pour les deux fichiers. Des conseils pour le fichier initializer.js ? Je pense que je suis vraiment proche ici, lors de la création d’un nouveau sujet, j’obtiens 1/3 des champs, le champ listingDetails mais il me manque toujours isClassifiedListing et 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 « J'aime »

Je ne l’ai pas testé, mais je pense que la raison pour laquelle vous ne voyez qu’un tiers des champs est qu’il boucle et enregistre une classe de connecteur non unique, remplaçant la précédente.

En général, pour le côté client, plutôt que de parcourir les champs personnalisés et de déclarer les méthodes d’API. Je suggère de définir des composants pour chaque champ séparément, ou du moins des actions séparées, car vous aurez probablement besoin d’une logique différente associée à chaque champ ?

La seule partie sur laquelle je bouclerais et déclarerais est la suivante :

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

Pour le reste des composants, il est probablement préférable de créer une logique distincte pour chaque cas.

3 « J'aime »

@keegan ça a fonctionné ! merci beaucoup pour toutes ces informations. Je n’aurais pas pu y arriver sans votre aide.

4 « J'aime »