Impostazione del tag predefinito per categoria in un componente del tema

Se visiti /tags/category-slug/tag-name e fai clic sul pulsante Nuovo argomento, il compositore ha il tag preimpostato, come descritto qui:

È fantastico. Ma ora io (e almeno un’altra persona) vorremmo poter impostare questo comportamento con un tag predefinito quando visitiamo /c/cat-slug/cat-id. Sembra che un componente del tema dovrebbe essere in grado di individuare quel pulsante e modificarlo oppure nasconderlo e aggiungere un nuovo pulsante (c’è un’uscita per plugin proprio lì, che non riesco a trovare ora, ma ho visto un minuto fa).

Qualcuno può darmi un suggerimento?

Dovrebbe funzionare solo su una categoria specifica, o avresti bisogno che supporti un “tag predefinito” per molte categorie, dove quel tag è diverso per ciascuna di esse?

Immagino che potrei creare un’impostazione per avere un tag predefinito per alcune categorie. Probabilmente posso farlo, ma non so dove o come modificare il pulsante ‘Crea argomento’ in modo che includa il tag predefinito.

TL;DR salta al codice funzionante qui


Quando visualizzi una pagina che contiene il pulsante + Nuovo Argomento, puoi ispezionare l’HTML tramite gli strumenti di sviluppo.

Se lo fai, noterai che ha un ID.

Gli ID sugli elementi HTML dovrebbero essere unici, ovvero… nessun due elementi nella stessa vista possono condividere lo stesso attributo ID HTML. Quindi, questo è sufficiente per iniziare.

Se cerco "create-topic" su Github, ecco cosa vedo…

Repository search results · GitHub

Notate il filtro a sinistra.

So che voglio rintracciare l’HTML del pulsante, quindi Handlebars, perché sto cercando di individuare l’azione che invia.

Quindi, seleziono Handlebars; poi vedo questo.

Repository search results · GitHub

C’è solo un risultato lì dentro, quindi siamo fortunati. Se ci fossero più risultati, ci sarebbero cose che si possono fare per restringere ulteriormente l’elenco, ma questo esula dall’ambito di questo argomento.

Quindi, controlliamo quel file.

Vedrai allora che l’azione del pulsante è impostata così

action=action

Bene… non è molto utile… Quindi, cosa facciamo ora?

Quando vedi action=action, significa che l’azione viene passata al componente da un template genitore.

Proviamo a vedere quali template contengono quel componente. Quindi, andiamo su Github e cerchiamo il nome del componente come verrebbe usato in un template. Per questo esempio, useremmo qualcosa del genere "{{create-topic-button"

Nota che ho aggiunto solo {{NOME_COMPONENTE e ho saltato il resto. Non conosciamo gli altri argomenti passati, quindi vogliamo una ricerca generica.

Ecco il risultato

Repository search results · GitHub

Otteniamo due risultati… uno di essi è nel plugin styleguide, quindi lo ignoriamo semplicemente. L’altro è nel core. Quindi, vediamo com’è fatto

discourse/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs at 292412f19610d49944f3e109aa7546ccd0553d6a · discourse/discourse · GitHub

  {{create-topic-button
    canCreateTopic=canCreateTopic
    action=(action "clickCreateTopicButton")
    disabled=createTopicButtonDisabled
    label=createTopicLabel
    btnClass=createTopicClass
    canCreateTopicOnTag=canCreateTopicOnTag
  }}

Ahh… stiamo arrivando vicini. Ora vedi che l’azione per il pulsante è

action=(action "clickCreateTopicButton")

Ora dobbiamo scoprire cosa fa quell’azione. Quindi, cerchiamo il nome dell’azione. Poi filtriamo fino ai file .js perché ora vogliamo vedere la definizione di quell’azione nel file JS del componente.

Repository search results · GitHub

Ancora una volta, otteniamo un solo risultato, quindi diamo un’occhiata.

Sembra che l’azione faccia una delle due cose. Se la categoria è in sola lettura e l’utente non ha già una bozza, mostra un avviso. Altrimenti, chiama un metodo createTopic().

Siamo interessati al secondo caso, quindi diamoci un’occhiata.

Se cerchi createTopic() in quel file (ricerca interna, non su Github)… noterai che c’è un solo riferimento per esso. Che succede? Come fa questo componente a chiamare un metodo non definito?

Bene, la risposta è più in alto nel file.

Cosa significa questo?

Non voglio perdere troppo tempo qui, ma Ember utilizza le Classi. Pensa alle classi come a pacchetti di codice riutilizzabili. Tutto ciò che la riga evidenziata sopra significa è:

Prendi il pacchetto Component di Ember, aggiungi il pacchetto FilterModeMixin ad esso e permettimi di aggiungere altri metodi, o sovrascrivere alcuni di quelli esistenti, al risultato per creare un nuovo componente Ember per la mia applicazione.

Quindi, torniamo all’azione che stiamo cercando di tracciare.

clickCreateTopicButton() {
  if (this.categoryReadOnlyBanner && !this.hasDraft) {
    bootbox.alert(this.categoryReadOnlyBanner);
  } else {
    this.createTopic();
  }
},

Chiamando this.createTopic(). questo non è un metodo predefinito del componente Ember. È un metodo Discourse personalizzato, quindi deve provenire da FilterModeMixin. Cos’è FilterModeMixin? Bene… è definito all’inizio del file.

import FilterModeMixin from "discourse/mixins/filter-mode";

Quindi, immagino che dobbiamo andare lì.

discourse/app/assets/javascripts/discourse/app/mixins/filter-mode.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

Ferma un attimo e fai una ricerca interna per createTopic() in quel file. Davvero. Smetti di leggere e fallo. Aspetterò… non barare… ho gli :eyes: su di te.


OK. Hai cercato e non ci sono stati risultati. Cosa facciamo ora?

Quello che ho descritto sopra è solo un metodo per passare le cose in basso. Se non trovi quello che cerchi. Fai un passo indietro e prova un approccio diverso.

Quindi, ricapitoliamo… dove siamo ora? Prima di rimanere bloccati, stavamo guardando il file JS per il componente d-navigation. Vediamo il suo template.

Ancora una volta, usiamo "{{NOME_COMPONENTE" e cerchiamo.

Repository search results · GitHub

Questo ci dà quattro risultati…

Importa? Forse. Importa per questo caso? No. Stiamo solo cercando di capire da dove viene createTopic() o cos’è. Quindi, procediamo con il primo risultato.

discourse/app/assets/javascripts/discourse/app/templates/navigation/default.hbs at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

Guardate un po’…

createTopic=(route-action "createTopic")

Ottimo… altro gergo… perché a tutti piace

Sul serio, però, parliamo delle route actions. Cosa sono? Beh. Sono route… actions? Ovvero azioni definite sulla route. Perché sono utili? Perché le route in Discourse possono essere nidificate

Vediamola così

- route-1
  - route-1-1
  - route-1-2
  - route-1-3

Se ho un componente condiviso che devo far funzionare sulle route 111, 112 e 113 con parametri diversi, non sarebbe più facile se definissi semplicemente lo stesso componente in tutti e lo passassi con la stessa azione? Poi lo modificassi per ogni route se necessario?

Questo è ciò che fanno le route-actions.

OK, torniamo alla domanda. Stavamo guardando

createTopic=(route-action "createTopic")

nel componente navigation/default.

Ora dobbiamo solo capire qual è la route per controllare cosa fa quella route action.

Vuoi modificare il comportamento del pulsante nuovo argomento nelle pagine /c/cat-slug/cat-id. Quindi, visitiamo una di quelle pagine. Ad esempio: http://localhost:4200/c/meta/6

Qual è questa route? A meno che tu non conosca molto bene Discourse, non potresti dirlo. Quindi, cosa facciamo ora?

È qui che l’estensione Ember per il tuo browser diventa utile.

Installala qui se non ce l’hai già. Aspetterò.
(il link è un repository Github, ma la descrizione contiene i link all’estensione per diversi browser)


OK, ora che l’hai installata, visita di nuovo quella pagina /c/cat-slug/cat-id e guarda la pagina dell’estensione.

Una volta caricata, clicca su Routes, poi attiva “Solo route corrente”

Ahhh… guarda un po’. Ora sappiamo su quale route siamo. Siamo su discovery.category

Ma non è tutta la storia… è

application > discovery > discovery.category

Ricorda, le route sono nidificate. Quindi, cosa facciamo ora?

Di solito inizio dall’alto. In questo caso, sarebbe la route application. Trova il file per quella route e cerca per vedere se l’azione è definita lì.

discourse/app/assets/javascripts/discourse/app/routes/application.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

Si scopre che non lo è… quindi ci spostiamo giù nell’albero di nidificazione alla route discovery.

discourse/app/assets/javascripts/discourse/app/routes/discovery.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

Cerca lì… e… bingo!

Ok, ora sappiamo cosa

createTopic=(route-action "createTopic")

riferisce. Quindi diamo un’occhiata a quell’azione.

createTopic() {
  if (this.get("currentUser.has_topic_draft")) {
    this.openTopicDraft();
  } else {
    this.openComposer(this.controllerFor("discovery/topics"));
  }
},

Sembra che stia facendo una delle due cose. Se l’utente ha una bozza, la apre. Altrimenti, chiama openComposer() con un parametro. Qual è il passo successivo? Beh, dovresti già conoscere la risposta. Dobbiamo scoprire da dove viene openComposer() o cosa fa.

Quindi cerchiamo nel file openComposer() e… ovviamente non otteniamo risultati. Non c’è un metodo in quella route chiamato openComposer()

Cosa facciamo dopo? Ricordi il discorso sulle Classi Ember? Proviamo quello.

Abbiamo questo in cima al file della route.

Questo significa che questa route eredita tutti i metodi dal pacchetto DiscourseRoute così come quelli definiti nel pacchetto OpenComposer

openComposer è più probabilmente ciò che vogliamo, quindi diamoci un’occhiata. Prima di farlo però… dobbiamo vedere come openComposer è definito in quel file.

import OpenComposer from "discourse/mixins/open-composer";

Guarda l’URL. Non è un componente Ember. Non è una route; non è un modello. È un mixin. Ma diavolo è un mixin? La risposta brevissima… è un pacchetto di funzioni riutilizzabili.

Li definisci nel tuo mixin.

add(number) {
  return number + 1
}

substract(number) {
  return number - 1
}

poi aggiungi il mixin al tuo componente Ember, poi puoi fare qualcosa del genere

// valore iniziale è 1
myMethod () {
  this.add(value) // restituisce 2
  this.substract(value) // restituisce 0 
}

Quindi, come si relaziona questo a ciò che stiamo cercando di fare?

Beh, open-composer qui.

import OpenComposer from "discourse/mixins/open-composer";

è un mixin. Uno dei metodi in quel mixin è OpenComposer()

Va bene se ti senti confuso a riguardo. Condividono lo stesso nome - tranne che uno inizia con una lettera maiuscola, il che indica che è una Classe.

Significano cose diverse.

Per capire questo, dovresti sapere che il nome che dai ai tuoi moduli importati non conta (in questo caso particolare), purché siano esportati come “default”

Spiegare questo va un po’ oltre l’ambito di questo argomento. Tutto quello che devi sapere è che questo.

OpenComposer qui

discourse/app/assets/javascripts/discourse/app/routes/discovery.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

e openComposer() qui

discourse/app/assets/javascripts/discourse/app/mixins/open-composer.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

non sono la stessa cosa.

OK… ricapitoliamo.

ID HTML pulsante Nuovo argomento < Azione pulsante Nuovo argomento < Azione componente d-navigation < Azione route discovery < Mixin OpenComposer < metodo openComposer()

Quindi… questo è il metodo che viene infine chiamato quando clicchi il pulsante + Nuovo Argomento su quella route.

openComposer(controller) {
  let categoryId = controller.get("category.id");
  if (
    categoryId &&
    controller.category.isUncategorizedCategory &&
    !this.siteSettings.allow_uncategorized_topics
  ) {
    categoryId = null;
  }
  this.controllerFor("composer").open({
    prioritizedCategoryId: categoryId,
    topicCategoryId: categoryId,
    action: Composer.CREATE_TOPIC,
    draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
    draftSequence: controller.get("model.draft_sequence") || 0,
  });
},

Quindi torniamo alla tua domanda.

Abbiamo stabilito come puoi capire l’azione per quel pulsante su /c/cat-slug/cat-id, ma sembra diverso da ciò che accade quando visiti /tags/category-slug/tag-name, che è ciò che vuoi fare.

Quindi qual è il prossimo passo? Vediamo cosa fa quella route per gestire l’azione createTopic().

Beh… noterai che gestisce l’azione in modo diverso.

per /c/cat-slug/cat-id sembra così

createTopic() {
  if (this.get("currentUser.has_topic_draft")) {
    this.openTopicDraft();
  } else {
    this.openComposer(this.controllerFor("discovery/topics"));
  }
},

per /tags/category-slug/tag-name sembra così

createTopic() {
  if (this.get("currentUser.has_topic_draft")) {
    this.openTopicDraft();
  } else {
    const controller = this.controllerFor("tag.show");
    const composerController = this.controllerFor("composer");
    composerController
      .open({
        categoryId: controller.get("category.id"),
        action: Composer.CREATE_TOPIC,
        draftKey: Composer.NEW_TOPIC_KEY
      })
      .then(() => {
        // Pre-compila il campo di input dei tag
        if (composerController.canEditTags && controller.get("model.id")) {
          const composerModel = this.controllerFor("composer").get("model");
          composerModel.set(
            "tags",
            [
              controller.get("model.id"),
              ...makeArray(controller.additionalTags)
            ].filter(Boolean)
          );
        }
      });
  }
}

Questa differenza è praticamente ciò che stai chiedendo qui.

Quindi, tutto quello che devi fare è… modificare l’azione createTopic() nella route discovery per farla funzionare come fa sulla route tag-show. Quindi come si fa?

Ricordi come abbiamo parlato di Ember che usa le Classi? Sì, dovremo tornare a quello di nuovo.

L’API del plugin ti permette di modificare le classi Ember tramite questo metodo.

https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L166-L195

Quindi, cosa stiamo cercando di modificare qui? La route discovery… perché… ricorda, è lì che è definita createTopic() quando sei su una pagina come /c/cat-slug/cat-id

Iniziamo con questo

api.modifyClass("route:discovery", {
  pluginId: "prefill-composer-tags",
  actions: {
    createTopic() {
      console.log("fires");
    }
  }
});

Cosa fa? Rompe il pulsante + Nuovo Argomento; tuttavia, ci dice che siamo nella direzione giusta. Se provi ad aggiungere il frammento sopra, noterai che cliccando il pulsante non si apre più il compositore. Invece, stampa solo un messaggio nella console. Questa è una cosa buona perché significa che abbiamo individuato la Classe giusta e l’azione giusta - route:discovery e createTopic()

Quindi, cosa facciamo dopo? Beh, ricorda che il pulsante su /tags/category-slug/tag-name fa esattamente ciò che vogliamo. Quindi, copiamo il codice da quella route - e aggiungiamo gli import necessari.

const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");
api.modifyClass("route:discovery", {
  pluginId: "prefill-composer-tags",
  actions: {
    createTopic() {
      if (this.get("currentUser.has_topic_draft")) {
        this.openTopicDraft();
      } else {
        const controller = this.controllerFor("tag.show");
        const composerController = this.controllerFor("composer");
        composerController
          .open({
            categoryId: controller.get("category.id"),
            action: Composer.CREATE_TOPIC,
            draftKey: Composer.NEW_TOPIC_KEY
          })
          .then(() => {
            // Pre-compila il campo di input dei tag
            if (composerController.canEditTags && controller.get("model.id")) {
              const composerModel = this.controllerFor("composer").get("model");
              composerModel.set(
                "tags",
                [
                  controller.get("model.id"),
                  ...makeArray(controller.additionalTags)
                ].filter(Boolean)
              );
            }
          });
      }
    }
  }
});

Funzionerà? No, ma siamo a un passo dalla soluzione. Perché non funziona? Perché i tag che aggiunge quando si apre il compositore non sono definiti. Perché? Perché sono caricati dal controller tag.show - che non è ciò che vogliamo. Modifichiamo il codice per farlo funzionare con la route su cui siamo.

Prima di farlo, però, ci serve una sorta di indice per i nostri tag predefiniti desiderati. Andiamo con un nuovo oggetto così

// category-slug: [ARRAY_TAG_PREDEFINITI]
const defaultTagIndex = {
  // slug a parola singola
  meta: ["a", "b", "c"],
  core: ["g", "h"],
  // slug con un trattino
  ["general-chat"]: ["d", "e", "f"]
};

Questo significa fondamentalmente che se il compositore viene aperto sulla pagina della categoria meta, aggiungi i tag “a,b,c”
Se il compositore viene aperto sulla pagina della categoria core, aggiungi i tag “g,h” e così via.

Ora che ce l’abbiamo, possiamo modificare l’azione per farla apparire così.

<h4 id="heading--final"Codice finale
const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");

// category-slug: [ARRAY_TAG_PREDEFINITI]
const defaultTagIndex = {
  // slug a parola singola
  meta: ["a", "b", "c"],
  core: ["g", "h"],
  // slug con un trattino
  ["general-chat"]: ["d", "e", "f"]
};

api.modifyClass("route:discovery", {
  pluginId: "prefill-composer-tags",
  actions: {
    createTopic() {
      try {
        const hasDraft = this.currentUser?.has_topic_draft;
        if (hasDraft) {
          this._super(...arguments);
          return;
        } else {
          const controller = this.controllerFor("discovery/topics");
          const composerController = this.controllerFor("composer");
          const categoryId = controller.category?.id;
          const categorySlug = controller.category?.slug;

          if (!categoryId) {
            this._super(...arguments);
            return;
          }

          composerController
            .open({
              categoryId: categoryId,
              action: Composer.CREATE_TOPIC,
              draftKey: Composer.NEW_TOPIC_KEY
            })
            .then(() => {
              // Pre-compila il campo di input dei tag
              if (composerController.canEditTags && categoryId) {
                const composerModel = composerController.model;
                composerModel.set(
                  "tags",
                  makeArray(defaultTagIndex[categorySlug]).filter(Boolean)
                );
              }
            });
        }
      } catch {
        this._super(...arguments);
        return;
      }
    }
  }
});

Note:

  1. Ho avvolto tutto in un blocco try…catch. Se il codice fallisce, eseguiamo this._super(...arguments)

  2. Se sei familiare con Ember, sapresti cosa fa this._super(...arguments). Se non lo sei, ecco una spiegazione semplice. Stiamo sovrascrivendo createTopic() quindi se le sovrascrizioni falliscono a causa di un errore - forse il core è stato aggiornato - allora torniamo al metodo nel core come definito qui

  3. Se l’utente ha una bozza di nuovo argomento, torniamo semplicemente a this._super(...arguments) e lasciamo che il core faccia il suo lavoro.

Questo dovrebbe essere sufficiente. Tutto quello che devi aggiungere ora è un modo per creare l’indice dei tag predefiniti tramite le impostazioni del tema.