Layouts Plugin

Hey @Alavi1412, if I’m understanding you correctly, you want to modify the profile widget to add a button that allows the user to open the composer to create a new topic?

Any button has two basic parts two it: the button element, and the button action. I think you already understand the button element part. As you say you’ll need to use the button widget with the appropriate classes, icon and label (note that the button widget doesn’t have a rawLabel, only a link does - go to the button widget file and search for ‘rawlabel’ and there will be no matches. Yes the ‘label’ requires a translation object). We’ll also need the button action, which refers to a method that handles what happens when the button is clicked.

This is the process I would take to figure out what we need for both the button element and the button action. I suggest you follow these steps yourself, as it will help you understand other issues you’ll encounter.

As always, the place we start is with the existing Discourse functionality we want to copy. In this case we start with the button in normal Discourse that opens the new topic compose when it is clicked.

  1. Go to meta.discourse.org and highlight the Create Topic button with the chrome inspector. You’ll see it has the id create-topic.

  2. Do a search for create-topic in the Discourse code. These are the results you’ll get

    How do we know which results are relevant? There’s often good hints in the name of the files. We know that the ‘Create Topic’ button appears in the same part of the app as the navigation controls, so it seems likely that the files with ‘navigation’ in the path are relevant. Those results also seem to be about button elements, which is what we’re after.

  3. Click through one of those relevant results and you’ll find button elements in templates that give us much of the information we need. e.g.:

    <button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
    

    We can simply copy / past these details to our button widget like so:

    this.attach('button', {
        className: 'btn btn-default',
        action: 'createTopic',
        label: 'topic.create',
        icon: 'plus'
      })
    

    Once we add that button widget to the profile widget, we now have a create topic button in the profile widget. We’re half the way there.

  4. If you click that new button, you’ll see a message in the console createTopic not found. We need to deal with the second part of the button, the button action. If you’ve read the part of the Ember Guide that covers Template Actions, you’ll know that the action handler we got from one of those templates refers to an action method in a corresponding component, controller or route. So let’s just do a search for that handler and find the corresponding instance.

    The search for createTopic will produce many results. It is useful to know that the part of the app with topic lists is called ‘discovery’. Also keep in mind that we’re looking for a method; any of the results are not methods. Scanning the results list the first one that really jumps out at me is this one (it’s both a method and it seems to be in the right place in the code:

    If you click through that result you’ll see a method like this:

    createTopic() {
      this.openComposer(this.controllerFor("discovery/topics"));
    },
    
  5. Ok we’ve now found the method. How do we get that method to work for our widget button? If you’ve read A tour of how the Widget (Virtual DOM) code in Discourse works, you’ll know that each action in a widget needs a corresponding action handler in the widget. Ok, so let’s create a createTopic method in the profile widget that does the same thing the createTopic method in the Discovery Route does.

    If you follow where the openComposer method in createTopic in the Discovery Route comes from (or open any of the other results in our search for createTopic methods in the codebase) you’ll figure you that what we need to do is send an ‘open’ action to the composer controller with the right parameters.

    this.controllerFor('composer').open({
      categoryId: controller.get('category.id'),
      action: Composer.CREATE_TOPIC,
      draftKey: controller.get('model.draft_key'),
      draftSequence: controller.get('model.draft_sequence')
    });
    

    So we need to access the composer controller in our widget action. The createTopic method in the discovery route also uses the discovery/topics controller, so we’ll need that too. If you look at the other action handlers in the profile widget you’ll see how they access controllers and routes from the widget. i.e.

    const cController = this.register.lookup('controller:composer');
    const dtController = this.register.lookup('controller:discovery/topics');
    

    Using this approach we can now make our own createTopic method.

      createTopic() {
        const cController = this.register.lookup('controller:composer');
        const dtController = this.register.lookup('controller:discovery/topics');
        cController.open({
          categoryId: dtController.get('category.id'),
          action: Composer.CREATE_TOPIC,
          draftKey: dtController.get('model.draft_key'),
          draftSequence: dtController.get('model.draft_sequence')
        });
      },
    

    The only part we’re missing now is the Composer object. That’s a reference to the composer model. All we need to do for that is import the composer model. Add the import at the top of the profile widget file

    import Composer from 'discourse/models/composer';
    

Once you add that new createTopic method and reload, your Create Topic button will now (mostly) work like the Create Topic button in the normal Discourse discovery. The composer will open. There are some issues that arise by virtue of the fact that the profile widget can appear in different contexts from the context in which the normal Create Topic button appears.

There are other ways to do the same thing we just did, however the key point here is:

  1. Start with what you already know. We know that there is a button in normal Discourse that does what we want to do. Start by figuring out how that works.

  2. Copy as much as possible. We can copy from both normal Discourse and the existing code in the profile widget that does similar things to what we want to do.

Using this approach the answers you get will not be 100% perfect, but they will get you 90% of the way there. Once you’re 90% there you have a much better idea of what the 10% of actual problems you’ll need to figure out are.

I have to get back to work now, so I’ll leave those other questions with you for now. Apply the same approach and see if you can figure out the answers.

12 Likes