How to add a super basic topic custom field

I’m trying to understand how to add a custom field to topics, and working through a very basic example. Goal: Add a custom field called “sample_field” to each topic created, with a simple string value.

I’ve reviewed various examples, like the poll plugin and the solved plugin and this discussion, but these plugins do so much more with their custom fields that I haven’t yet figured out the basic code you need.

So I’m not quite there–my plugin.rb file is missing something (I think), and I haven’t yet figured out how to bind the string value to the custom_field at the time the topic is created.

What do I need to make this work?

Any help is appreciated. Thanks!

Here’s what I’ve got

plugin.rb:
//create the custom field:

  after_initialize do
     Topic.register_custom_field_type('sample_field', :string)
     add_to_serializer(:topic_view, :custom_fields) { object.custom_fields } //if I want to show the custom field on the client side
  end

assets/javascripts/initializers/topic-custom-field.js.es6:
//initialize the custom fields object, and make it so it can be sent to the server:

import { withPluginApi } from 'discourse/lib/plugin-api';

export default {
  name: 'topic-custom-field',
  initialize() {
    withPluginApi('0.8.31', api => {
      api.modifyClass('model:topic', {
        custom_fields: {},
        asJSON() {
          return Object.assign(this._super(), {
            custom_fields: this.custom_fields
          });
        }
      })
    })
  }
}

Then, how do I add the value of the custom field to the topic at the time it is saved?

I believe the key “save” action for a topic happens here in the codebase:

app/templates/composer.hbs:

  <div class="save-or-cancel">
            {{#unless model.viewFullscreen}}
              {{composer-save-button action=(action "save")
                                     icon=saveIcon
                                     label=saveLabel
                                     disableSubmit=disableSubmit}}
...

How do I do something (in this case, add a value to the custom field) when that “save” action occurs?

I’ve tried creating a js file under initializers, where I could do something like:

api.modifyClass('component:composer-save-button', {
   actions: {
       topic.set('custom_fields.sample_field', 'here's a value for the sample_field')
   }
}

But I need to be able to link that “topic.set” to the “save” action in composer.hbs, and I don’t know how to do that.


And if there’s a simpler way to do this, I’m happy to hear it!

2 Likes

I’m not aware of a perfect example, but might try to develop one at some point.

You might have a look at some other plugins to look for examples. I’ll not sure which one off hand. There’s a Discourse repo called something like all-the-plugins, I often grep it to look for stuff.

I bet the reactions plugin that’s in testing here now is a good example. See the banner and have a look at that.

2 Likes

Yes, always look at existing plugins.

Here’s an example:

https://github.com/paviliondev/discourse-locations/blob/af2bb17c181459ac25a140d5f8a2e1ff0b42e5f1/plugin.rb#L119

Which relies on this connector and component:

https://github.com/paviliondev/discourse-locations/blob/af2bb17c181459ac25a140d5f8a2e1ff0b42e5f1/assets/javascripts/discourse/connectors/edit-topic/edit-location-details.hbs#L1

2 Likes

Thanks all–I’ll take a look at these. I mentioned in the original post that I had reviewed a bunch of other examples already. Any thoughts on the code I provided?

2 Likes

Sorry. I didn’t see any obvious (to me) issues. Though I’m getting better at plugin development, I often feel I know just enough to be dangerous.

2 Likes

Potentially over complicated if you can settle for the ‘Discourse way of doing things’, that is: edit the data within the Topic Meta area, and later only when the pencil icon is hit.

You only need to render an input box that’s correctly wired up and make sure you’ve serialized the value to the Topic View.

The PostRevisor code will do the magic of updating the custom field.

Look at the Locations plugin example. Even that is more than you need.

Make sure you run the Locations plugin too to see how that looks.

2 Likes

Here’s another example, with a slightly different input scenario, but still leveraging the same mechanics and hooks:

https://github.com/paviliondev/discourse-topic-previews/blob/master/assets/javascripts/discourse/connectors/edit-topic/select-thumbnail-connector.hbs

https://github.com/paviliondev/discourse-topic-previews/blob/6064a5940a80e69ed56a7cbe67f8ded8e33e5d67/plugin.rb#L82

4 Likes

Thanks. I’m not familiar with PostRevisor. Looking into that now.

Not sure what you mean by Topic Meta area. Is that a file you have in mind? (In terms of ui, I know there’s the composer for creating, with the d-editor in there, and then there is the topic view itself. Not sure where the “Meta” area is in there.)

2 Likes

I consider Category and Tag information to be meta data.

2 Likes

Here’s a currently working basic example of creating a topic custom field, and then setting it. This sets the field when the user clicks a particular button on the topic view page.

I note that in the plugins that have topic custom fields, there’s a common use in plugin.rb of PostRevisor (like PostRevisor.track_topic_field) and DiscourseEvent.on(:topic_created) do |topic|... These might allow setting the custom field without having to hit a button–such as setting the custom field when the topic is created. But I haven’t figured that out yet.

[EDIT: If anyone wants to suggest code for plugin.rb to add the custom field value to a topic at the time of creating the topic (instead of having a separate button that needs to be clicked), please do! :slight_smile: ]

So here’s a basic example that works without that fancy stuff:

plugin.rb

Topic.register_custom_field_type('sample_field', :string)
add_to_class(:topic, :sample_field) { self.custom_fields['sample_field'] } ##maybe not necessary. based on line 83 of discourse-locations plugin plugin.rb
	
add_to_serializer(:topic_view, :sample_field, false) { object.topic.sample_field } ##probably only necessary if you want to show the custom field result to the user

connector/[add-button].hbs

//example here: connector is topic-above-post-stream, which passes a model (topic) in the plugin outlet

<button {{action "setThatTopic" model}}>Click Here</button>

connector/[add-button].js.es6

import Topic from 'discourse/models/topic';  //may not be necessary

export default {			
    actions: {
       setThatTopic(model){
           model.set('custom_fields.sample_field', 'newValue123')
           console.log('testing that custom field is there = ' + JSON.stringify(model.custom_fields))
        }
     }
}

assets/javascripts/initializers/topic-custom-field.js.es6

//this might not be necessary. But the idea is to initialize the custom fields object, and make it so it can be sent to the server:

import { withPluginApi } from 'discourse/lib/plugin-api';

export default {
    name: 'topic-custom-field',
     initialize() {
	   withPluginApi('0.8.31', api => {
		 api.modifyClass('model:topic', {
			custom_fields: {},
			asJSON() {
			    return Object.assign(this._super(), {
				custom_fields: this.custom_fields
			   });
			}
	     })
	   })
    }
}
6 Likes

I just published an “education plugin” demonstrating how this is done

8 Likes

Thank you, @angus. This is tremendously helpful, and just the type of thing to make coding with discourse easier. I’d personally love to see this type of resource–a to-the-point, bare-bones, code example of implementing a single feature at a time–for a variety of tasks with discourse. For example, custom fields for categories and groups, too. This type of resource is a huge time-saver.

3 Likes