Wie man benutzerdefinierte Felder zu Modellen hinzufügt

This is a collection of education plugins that demonstrate how to add a custom field to different models in Discourse. They are intended as learning tools for those looking to learn how to build discourse plugins.

GitHub-Mark How to add a custom field to a topic
GitHub-Mark How to add a custom field to a category

Who they’re for

These plugins are for people looking to learn more about creating Discourse plugins. Before you start working with these plugins, you should complete the beginners guide to creating discourse plugins.

You could use these plugins just to add custom fields on your Discourse instance, however you would still need to change the code slightly to do so. It’s not designed for plugin-and-play use on a live server.

How they work

As well as containing working code, each plugin contains a step-by-step description of what the code is doing in the form of comments. For example

## 
# 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)

Hopefully the steps and comments are self explanatory. The references are there to show you where to look if you want to learn more.

Let me know if you find it useful, if something isn’t working, or the notes are unclear :slight_smile:

28 „Gefällt mir“

Thank for build plugin cho our,
i have this error later install plugin:

Oops
The software powering this discussion forum encountered an unexpected problem. We apologize for the inconvenience.
Detailed information about the error was logged, and an automatic notification generated. We’ll take a look at it.
No further action is necessary. However, if the error condition persists, you can provide additional detail, including steps to reproduce the error, by posting a discussion topic in the site’s feedback category.

Hey, are you running this in your local development environment? If so, could you PM your development logs? If your dev environment is set up correctly, this plugin will work.

1 „Gefällt mir“

I read, but i can’t catch it. Can you spending time make a video tutorial install this plugin from make local development environment to step by step add a custom field …
So difficult to access this plugin.
If possible, can you upgrade search feature for field type is number?

I can donate for this plugin, please support help me!

This resource that @angus has put together is even more helpful than I first realized.

Not only is the code for adding a custom field there with clear explanations, but a lot of it can be plugged directly into any plugin, because the code mostly uses variables like FIELD_NAME and FIELD_VALUE, which you can define in plugin/config/settings.yml (you also need to make sure that your plugin file structure is the same as in the github code @angus has provided). Going through the code has also given me a greater understanding of some discourse functions and methods I have seen before, but never really understood until now.

So far, the code works great to create and save topic custom fields. There are two questions that have been coming up for me:

  1. Topic List Error: It seems to throw an error in the case I try to load a category list (ie, a topic list for a category) where there are topics created prior to adding the custom field. It shows the exception page, and lists this error:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. What’s the recommended way to solve this one?

  2. Is there a way for me to apply the custom field only for topics in certain categories? So, let’s say I have Category 1, Category 2, and Category 3, and I only want the custom field input to show up and only want the field to be saved if the topic is part of Category 3. Is there a way to do that?

1 „Gefällt mir“

We (Pavilion) will be doing something like that in the future, but for now it’s just the code and the steps. If you’re stuck on a specific issue, create a post in Dev and describe the issue in some detail.

I’ve added a sub-step to the topic custom fields plugin demonstrating how to preload fields if you’re using them in the topic list.

https://github.com/pavilionedu/discourse-topic-custom-fields/commit/bd34679589bcf27d63e922533856569495a77a76

You’ll need a category custom field to identify categories it should be displayed in. I’ve given category custom fields the same treatment in this plugin, complete with the same step-by-step instructions:

https://github.com/pavilionedu/discourse-category-custom-fields

Combining these two education plugins won’t take you all the way to your goal, but see if you can make it from there.

2 „Gefällt mir“

That is fantastic, @angus. Thank you very much.

A video is always nice–I’m all for making things as straightforward as possible, but I don’t think it’s required to get the key value from these resources that @angus has put together. These resources give the code that you need to accomplish the specific goal the resource is about (having a working topic custom field or a category custom field). A video would probably just be @angus or someone else talking through how to implement the resource, but that is straightforward, and we can probably just lay it out here.

To be clear, these resources are not plugins that you just add to your site as a plug and play that customizes your forum. Rather, they efficiently give you the understanding you need to code your own custom fields in your plugin.


This is how I’ve used these resources:

You will need to add in the name and type of field that you want in config/settings. The code in these resources uses variables that are defined there. So you actually don’t need to do much customizing of the code to get it to work in your own plugin after that–the variables in plugin.rb and elsewhere refer to config/settings, and then should work.

After updating config/settings, you can just follow along with the code, adding it to your plugin:

  • Start with the code in plugin.rb, and add that to your own plugin’s plugin.rb in order to create the custom field

  • Then go to the initializer (at assets/javascripts/discourse/[custom-field-initiliazer]) to get the code that will initialize the custom field and allow you to save it to the server

  • Then create the form in the view layer that will be where the user (or your app, if the app adds the field automatically) can enter the value for the custom field, here (assets/discourse/connectors/[plugin-outlet-name]/[your special template].hbs.

  • @angus has set these up so you would add the forms for the custom fields in a plugin outlet that will be inserted in the discourse template. Settings for this form are here, (assets/javascripts/discourse/lib/[custom-field-name].js.es6, so you probably want to customize that too to make the form work.

@angus feel free to correct anything I’ve said here.

Once I got the feel for setting up the custom field by going through the steps above, I then started customizing things a bit further (for example, getting more creative with how the form works), but this was an extremely helpful starting point that saved me hours of work.

After going through it, I did have some questions (like I asked earlier), but getting responses in dev seems like the most helpful way to go about things from there.

3 „Gefällt mir“

Great description! Yup, that’s how they’re intended to be used :+1:

1 „Gefällt mir“

Edit: I originally posted my question regarding how to retrieve items based on a custom field here, but decided the question was sufficiently different to warrant its own post. So I posted separately here.

2 „Gefällt mir“

I’m experiencing strange behavior with the composer following the topic custom fields example.

When I hit the “create topic” button (for example, on the category show page–but anywhere on the site), the composer fails to open, and I get this 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

I first started seeing this error when I tried to add a new ‘create topic’ button on a new page, but since then, even I remove that new button and even if I remove any related code, the error persists.

In some way, I think the following code–from the topic-custom-field-initializer–is causing the problem:

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

When I remove this code, the create topic buttons work again (properly opening the composer). When I put the code back in, the composer error comes back.

I have had this code in my plugin previously without problem. But now it causes the composer error (even if I have removed any composer or create-topic button -related code in my plugin).

Of course, this code is important–it serializes the custom field. But it seems that it is conflicting with composer. Any ideas on how to fix?

I was able to figure out the cause–I was trying to add two separate custom fields to the topic custom fields initializer. For some reason that was causing some interference. There probably is a way to properly add two custom fields to that file, but my code, just repeating the same code for two separate custom fields, was causing problems. It worked again fine when I removed the second custom field from that file.

Müsste jedes Feld mit diesem Skeleton-Code ein eigenständiges Plugin sein, wenn ich mehrere Felder hinzufügen wollte?

Ich freue mich sehr, dieses Tutorial gefunden zu haben, und frage mich, wie viel Anpassung ich an diesen Vorlagen vornehmen müsste, damit sie für benutzerdefinierte Benutzerfelder funktionieren.

Nein, Sie müssen nur zusätzlichen Code für das zusätzliche Feld hinzufügen. In den meisten Fällen durch einfaches Duplizieren des vorhandenen Codes, z. B.

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

Der erste Anlaufpunkt für benutzerdefinierte Benutzerfelder ist /admin/customize/user_fields, das Ihnen eine Benutzeroberfläche zum Hinzufügen bietet. Wenn Sie eine detailliertere Kontrolle wünschen, ähnelt der Prozess dem für Themen und Kategorien, aber Sie benötigen keine Frontend-Elemente für Benutzerfelder.

Tatsächlich denken wir (Pavilion) darüber nach, ein Plugin für benutzerdefinierte Felder zu entwickeln (analog zu ACF für WordPress), das zunächst der Benutzeroberfläche für benutzerdefinierte Felder im Custom Wizard Plugin ähneln würde.

Tatsächlich verwenden einige Leute bereits das Custom Wizard Plugin als Manager für benutzerdefinierte Felder. Es listet alle benutzerdefinierten Felder auf Ihrer Instanz (aus beliebiger Quelle) auf und ermöglicht Ihnen, ein Feld eines beliebigen Typs zu jedem Modell hinzuzufügen, das sie unterstützt.

Es fügt keine Frontend-Unterstützung hinzu, z. B. wie im Bildungs-Plugin für benutzerdefinierte Themenfelder gezeigt (und das würde im Kontext des Custom Wizard Plugins nicht funktionieren), weshalb wir darüber nachdenken, dies in ein separates Plugin auszulagern.

3 „Gefällt mir“

@angus, ich glaube, das würde mir sehr gefallen.

Besonders, wenn es die Frontend-Unterstützung hinzufügt.

Ich hätte gerne eine einfache Möglichkeit für Administratoren, benutzerdefinierte Felder zu verschiedenen Klassen hinzuzufügen, Benutzern die Eingabe zu ermöglichen (z. B. für Themen, Beiträge, Benutzerprofile) und eine Möglichkeit für das Frontend, diese anzuzeigen.

Das Hauptproblem, das ich mit den aktuellen benutzerdefinierten Benutzerfeldern nicht lösen kann, ist die Vielfalt der Feldtypen. Derzeit ist die Auswahl auf 4 beschränkt, glaube ich, und ich hätte gerne die Optionen, die mit dem Custom Wizards Plugin verfügbar sind.

Idealerweise möchte ich ein ziemlich fortschrittliches, durchsuchbares/filterbares/sortierbares Benutzerverzeichnis mit vielen benutzerdefinierten Feldern verschiedener Typen erstellen. Ich werde mit Custom Wizards experimentieren, um zu sehen, ob es vorerst funktioniert, und hoffe, dass Sie in das Custom Fields Plugin investieren werden.

Danke!

@angus

Zuerst einmal vielen Dank für dieses Plugin.

Haben Sie vielleicht ein funktionierendes Beispiel dieses Plugins (benutzerdefiniertes Feld zu Thema), um mehrere benutzerdefinierte Felder zu berücksichtigen? Ich konnte erfolgreich ein benutzerdefiniertes Feld hinzufügen und ein paar Modifikationen ohne Probleme vornehmen.

Ich habe versucht, den Code zu duplizieren, zu modifizieren und ein zusätzliches Plugin hinzuzufügen usw.

Hat jemand ein Code-Repository oder ein Beispiel, das er mit mir teilen möchte? Jede Hilfe wäre sehr dankbar.

1 „Gefällt mir“

Hallo @Joe_Stanton,

Ich habe das schon ein paar Mal gemacht und die Art und Weise, wie ich es gemacht habe, war, die benutzerdefinierten Felder in einem Array mit einem Objekt mit einem Namen und Typ für das benutzerdefinierte Feld zu speichern.

Zum Beispiel:

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

Dann durchlaufe ich die Felder, um die Logik anzuwenden, die in diesem Thema erwähnt wird. Sie können ein Beispiel dafür sehen, wie es in einem Plugin verwendet wird, an dem ich arbeite hier. Der relevante Code ist jedoch unten aufgeführt.

Beispiel

Auf der Serverseite:

  # Registrierung benutzerdefinierter Felder
  fields.each do |field|
    # Registriere die Felder
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

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

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

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

    # Aktualisierung bei Themen-Bearbeitung
    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

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

    # Vorladen der Felder
    add_preloaded_topic_list_custom_field(field[:name])

    # Serialisierung zur Themenliste
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

Ähnlich auf der Clientseite, um die Felder zu serialisieren:


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

  // Benutzerdefinierte Felder serialisieren:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 „Gefällt mir“

Vielen Dank, @keegan!

Ich glaube, ich habe die Datei plugin.rb richtig eingerichtet, die Schleife sieht korrekt aus.

Ich habe jedoch Schwierigkeiten mit der Datei topic-custom-field-initializer.js. Hier ist mein Code für beide Dateien. Irgendwelche Hinweise für die Initialisierungsdatei initializer.js? Ich glaube, ich bin hier wirklich nah dran. Wenn ich ein neues Thema erstelle, erhalte ich 1 von 3 Feldern, das listingDetails-Feld, aber die Felder isClassifiedListing und listingStatus fehlen immer noch.

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 „Gefällt mir“

Ich habe es nicht getestet, aber ich glaube, der Grund, warum Sie nur 1/3 der Felder sehen, ist, dass es über die benutzerdefinierten Felder schleift und eine nicht eindeutige Connector-Klasse registriert und die vorherige überschreibt.

Im Allgemeinen schlage ich für die Client-Seite vor, anstatt durch die benutzerdefinierten Felder zu schleifen und die API-Methoden zu deklarieren, Komponenten für jedes Feld separat zu definieren oder zumindest separate Aktionen, da Sie wahrscheinlich unterschiedliche Logik für jedes Feld benötigen werden?

Der einzige Teil, über den ich schleifen und deklarieren würde, ist dieser:

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

Für den Rest der Komponenten ist es wahrscheinlich am besten, für jeden Fall eine separate Logik zu erstellen.

3 „Gefällt mir“

@keegan das hat funktioniert! Vielen Dank für all die Einblicke. Ohne deine Hilfe hätte ich es nicht geschafft.

4 „Gefällt mir“