كيفية إضافة حقول مخصصة إلى النماذج

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 إعجابًا

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)

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)

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)

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 إعجابات

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

إعجاب واحد (1)

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)

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.

مع هذا الرمز الهيكلي، إذا أردت إضافة حقول متعددة، فهل يجب أن يكون كل حقل بمثابة إضافة مستقلة؟

أنا متحمس جدًا للعثور على هذا البرنامج التعليمي وأتساءل، ما مقدار التعديلات، إن وجدت، التي سأحتاج إلى إجرائها على هذه القوالب لكي تعمل مع حقول المستخدم المخصصة؟

لا، تحتاج فقط إلى إضافة رمز إضافي للحقل الإضافي. في معظم الحالات، يكون ذلك بمجرد تكرار الرمز الموجود، على سبيل المثال:

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

أول مكان للبحث عن حقول المستخدم المخصصة هو /admin/customize/user_fields والذي يمنحك واجهة مستخدم لإضافتها. إذا كنت ترغب في الحصول على تحكم أكثر دقة، فإن العملية تبدو مشابهة جدًا للموضوع والفئة، ولكنك لا تحتاج فعليًا إلى عناصر الواجهة الأمامية مع حقول المستخدم.

في الواقع، نحن (Pavilion) نفكر في إنشاء إضافة للحقول المخصصة (على غرار ACF لـ WordPress) والتي ستبدو في البداية مثل واجهة إدارة الحقول المخصصة في إضافة Custom Wizard.

في الواقع، يستخدم بعض الأشخاص بالفعل إضافة Custom Wizard كمدير للحقول المخصصة. تسرد جميع الحقول المخصصة في مثيلك (من أي مصدر) وتسمح لك بإضافة حقل من أي نوع إلى أي نموذج يدعمها.

إنها لا تضيف دعمًا للواجهة الأمامية، على سبيل المثال، مثل ذلك المعروض في إضافة تعليم Topic Custom Field (ولن يعمل ذلك في سياق إضافة Custom Wizard)، ولهذا السبب نفكر في فصل ذلك إلى إضافة منفصلة.

3 إعجابات

@angus، أعتقد أنني سأحب هذا.

خاصة إذا أضاف دعم الواجهة الأمامية.

أود أن تكون هناك طريقة سهلة للمسؤولين لإضافة حقول مخصصة لفئات مختلفة، والسماح للمستخدمين بملئها (على سبيل المثال، للمواضيع، المشاركات، ملفات تعريف المستخدمين)، وأن تكون هناك طريقة للواجهة الأمامية لعرضها.

الشيء الرئيسي الذي لا يمكنني الحصول عليه مع حقول المستخدم المخصصة الحالية هو مجموعة متنوعة من أنواع الحقول. حاليًا، يقتصر على 4 أعتقد، وأود أن أحصل على الخيارات المتاحة مع إضافة Custom Wizards.

من الناحية المثالية، أريد بناء دليل مستخدم متقدم جدًا قابل للبحث/التصفية/الفرز مع الكثير من الحقول المخصصة من أنواع عديدة، وسأجرب Custom Wizards لمعرفة ما إذا كانت ستعمل في الوقت الحالي وآمل أن تستثمروا في إضافة Custom Fields.

شكرًا!

@angus

أولاً، شكراً جزيلاً لك على هذه الإضافة.

هل لديك مثال عملي لهذه الإضافة (حقل مخصص إلى موضوع) لاستيعاب حقول مخصصة متعددة؟ تمكنت من إضافة حقل مخصص واحد بنجاح وإجراء بعض التعديلات دون مشكلة.

لقد حاولت تكرار الكود، وتعديله وإضافة إضافة إضافية، وما إلى ذلك..

هل لدى أي شخص مستودع كود أو مثال يرغب في مشاركته معي؟ أي مساعدة ستكون موضع تقدير كبير.

إعجاب واحد (1)

مرحباً @Joe_Stanton،

لقد فعلت ذلك عدة مرات، والطريقة التي فعلت بها ذلك كانت تخزين الحقول المخصصة في مصفوفة مع كائن له اسم ونوع للحقل المخصص.

على سبيل المثال:

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

    # التسلسل إلى قائمة الموضوعات
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

وبالمثل من جانب العميل لتسلسل الحقول:


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

  // تسلسل الحقول المخصصة:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
إعجابَين (2)

شكراً @keegan!

أعتقد أنني قمت بإعداد ملف plugin.rb بشكل صحيح، وتبدو الحلقة صحيحة.

ومع ذلك، أواجه صعوبة في ملف topic-custom-field-initializer.js. إليك الكود الخاص بي لكلا الملفين. هل لديك أي اقتراحات لملف initializer.js؟ أعتقد أنني قريب جدًا هنا، عند إنشاء موضوع جديد، أحصل على حقل واحد من أصل ثلاثة، وهو حقل listingDetails، لكن لا يزال ينقصني isClassifiedListing و 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)

لم أختبر ذلك، لكنني أعتقد أن السبب في رؤيتك لحقل واحد فقط من أصل ثلاثة هو أنه يمر عبر فئة موصل غير فريدة ويسجلها ويتجاوز السابقة.

بشكل عام، بالنسبة للجانب العميل، بدلاً من المرور عبر الحقول المخصصة وتحديد طرق واجهة برمجة التطبيقات (API). أقترح عليك تعريف مكونات لكل حقل على حدة، أو على الأقل إجراءات منفصلة حيث ستحتاج على الأرجح إلى منطق مختلف مرتبط بكل حقل؟

الجزء الوحيد الذي سأمر عبره وأعلنه هو هذا:

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

بالنسبة لبقية المكونات، من الأفضل على الأرجح إنشاء منطق منفصل لكل حالة.

3 إعجابات

@keegan لقد نجح الأمر! شكراً جزيلاً على كل الأفكار. لم أكن لأتمكن من فعل ذلك بدون مساعدتك.

4 إعجابات