Come aggiungere campi personalizzati ai modelli

Questa è una raccolta di plugin educativi che dimostrano come aggiungere un campo personalizzato a diversi modelli in Discourse. Sono pensati come strumenti di apprendimento per chi desidera imparare a creare plugin per Discourse.

GitHub-Mark Come aggiungere un campo personalizzato a un argomento
GitHub-Mark Come aggiungere un campo personalizzato a una categoria

A chi sono destinati

Questi plugin sono rivolti a chi vuole approfondire la creazione di plugin per Discourse. Prima di iniziare a lavorare con questi plugin, dovresti completare la guida per principianti alla creazione di plugin per Discourse.

Potresti utilizzare questi plugin semplicemente per aggiungere campi personalizzati alla tua istanza di Discourse, ma dovresti comunque modificare leggermente il codice per farlo. Non sono progettati per un utilizzo immediato (plugin-and-play) su un server attivo.

Come funzionano

Oltre a contenere codice funzionante, ogni plugin include una descrizione passo dopo passo di ciò che fa il codice, sotto forma di commenti. Ad esempio:

## 
# type:        step
# number:      1
# title:       Register the field
# description: Dove diciamo a Discourse che tipo di campo stiamo aggiungendo.
#              Puoi registrare un campo di tipo stringa, intero, booleano o JSON.
# references:  lib/plugins/instance.rb,
#              app/models/concerns/has_custom_fields.rb
##
register_topic_custom_field_type(FIELD_NAME, FIELD_TYPE.to_sym)

Speriamo che i passaggi e i commenti siano chiari da soli. I references sono presenti per indicarti dove guardare se desideri approfondire.

Fammi sapere se lo trovi utile, se qualcosa non funziona o se le note non sono chiare :slight_smile:

28 Mi Piace

Grazie per aver creato il plugin per noi,
hò questo errore dopo l’installazione del plugin:

Oops
Il software che alimenta questo forum di discussione ha riscontrato un problema imprevisto. Ci scusiamo per l’inconveniente.
Le informazioni dettagliate sull’errore sono state registrate e è stata generata una notifica automatica. Ce ne occuperemo.
Non è necessario intraprendere ulteriori azioni. Tuttavia, se l’errore persiste, puoi fornire ulteriori dettagli, inclusi i passaggi per riprodurre l’errore, pubblicando un argomento di discussione nella categoria feedback del sito.

Ehi, lo stai eseguendo nel tuo ambiente di sviluppo locale? Se sì, potresti inviarmi in privato i log di sviluppo? Se il tuo ambiente di sviluppo è configurato correttamente, questo plugin funzionerà.

1 Mi Piace

Ho letto, ma non riesco a capire. Potresti dedicare del tempo a creare un video tutorial su come installare questo plugin, partendo dall’ambiente di sviluppo locale e aggiungendo passo dopo passo un campo personalizzato…
È molto difficile accedere a questo plugin.
Se possibile, potresti migliorare la funzione di ricerca per il tipo di campo numerico?

Posso fare una donazione per questo plugin, per favore aiutami!

Questa risorsa preparata da @angus si è rivelata ancora più utile di quanto avessi inizialmente pensato.

Non solo è presente il codice per aggiungere un campo personalizzato, corredato da spiegazioni chiare, ma gran parte di esso può essere integrato direttamente in qualsiasi plugin, poiché utilizza prevalentemente variabili come FIELD_NAME e FIELD_VALUE, che è possibile definire nel file plugin/config/settings.yml (assicurati inoltre che la struttura dei file del tuo plugin corrisponda a quella del codice GitHub fornito da @angus). Analizzando il codice ho anche acquisito una maggiore comprensione di alcune funzioni e metodi di Discourse che avevo già visto, ma che non avevo mai realmente compreso fino ad ora.

Finora, il codice funziona perfettamente per creare e salvare campi personalizzati per i topic. Tuttavia, mi sono sorte due domande:

  1. Errore nell’elenco dei topic: Sembra che venga generato un errore quando provo a caricare un elenco di categorie (cioè un elenco di topic per una categoria) in cui esistono topic creati prima dell’aggiunta del campo personalizzato. Viene visualizzata la pagina delle eccezioni con il seguente messaggio di errore:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. Qual è il metodo consigliato per risolvere questo problema?

  2. Esiste un modo per applicare il campo personalizzato solo ai topic di determinate categorie? Quindi, supponiamo di avere Categoria 1, Categoria 2 e Categoria 3, e desideri che l’input del campo personalizzato venga visualizzato e che il campo venga salvato solo se il topic appartiene alla Categoria 3. È possibile farlo?

1 Mi Piace

Noi (Pavilion) faremo qualcosa di simile in futuro, ma per ora abbiamo solo il codice e le istruzioni. Se sei bloccato su un problema specifico, crea un post in Development e descrivi il problema in modo dettagliato.

Ho aggiunto un sottopassaggio al plugin dei campi personalizzati per i topic che dimostra come precaricare i campi se li stai utilizzando nell’elenco dei topic.

Avrai bisogno di un campo personalizzato per le categorie per identificare quelle in cui deve essere visualizzato. Ho applicato lo stesso approccio ai campi personalizzati per le categorie in questo plugin, completando le istruzioni passo dopo passo:

Combinare questi due plugin educativi non ti porterà direttamente al tuo obiettivo finale, ma prova a vedere se riesci a completare il percorso partendo da qui.

2 Mi Piace

È fantastico, @angus. Grazie mille.

Un video è sempre una bella cosa: sono totalmente favorevole a rendere le cose il più semplici possibile, ma non credo sia necessario per ottenere il valore chiave da queste risorse che @angus ha raccolto. Queste risorse forniscono il codice necessario per raggiungere l’obiettivo specifico di ciascuna risorsa (avere un campo personalizzato per i topic o per le categorie funzionante). Un video probabilmente consisterebbe semplicemente in @angus o qualcun altro che spiega come implementare la risorsa, ma è abbastanza semplice e possiamo probabilmente esporlo direttamente qui.

Per essere chiari, queste risorse non sono plugin che puoi semplicemente aggiungere al tuo sito come soluzione plug-and-play per personalizzare il forum. Piuttosto, ti forniscono in modo efficiente la comprensione necessaria per codificare i tuoi campi personalizzati nel tuo plugin.


Ecco come ho utilizzato queste risorse:

Dovrai aggiungere il nome e il tipo di campo che desideri in config/settings. Il codice in queste risorse utilizza variabili definite lì. Quindi, dopo averlo fatto, in realtà non hai bisogno di personalizzare molto il codice per farlo funzionare nel tuo plugin: le variabili in plugin.rb e altrove fanno riferimento a config/settings e dovrebbero funzionare.

Dopo aver aggiornato config/settings, puoi semplicemente seguire il codice e aggiungerlo al tuo plugin:

  • Inizia con il codice in plugin.rb e aggiungilo al file plugin.rb del tuo plugin per creare il campo personalizzato.

  • Poi vai all’initializer (in assets/javascripts/discourse/[custom-field-initializer]) per ottenere il codice che inizializza il campo personalizzato e ti permette di salvarlo sul server.

  • Poi crea il form nel layer di visualizzazione dove l’utente (o la tua app, se l’app aggiunge il campo automaticamente) potrà inserire il valore per il campo personalizzato, qui (assets/discourse/connectors/[plugin-outlet-name]/[il-tuo-template-speciale].hbs).

  • @angus ha configurato tutto in modo che tu debba aggiungere i form per i campi personalizzati in un plugin outlet che verrà inserito nel template di Discourse. Le impostazioni per questo form si trovano qui (assets/javascripts/discourse/lib/[nome-campo-personalizzato].js.es6), quindi probabilmente vorrai personalizzare anche questo per far funzionare il form.

@angus, sentiti libero di correggere qualsiasi cosa io abbia detto qui.

Una volta preso confidenza con la configurazione del campo personalizzato seguendo i passaggi sopra, ho iniziato a personalizzare ulteriormente le cose (ad esempio, diventando più creativo nel modo in cui funziona il form), ma questo è stato un punto di partenza estremamente utile che mi ha fatto risparmiare ore di lavoro.

Dopo averlo completato, ho avuto alcune domande (come ho chiesto prima), ma ottenere risposte in Development sembra il modo più utile per procedere da quel punto.

3 Mi Piace

Ottima descrizione! Sì, è così che sono pensati per essere usati :+1:

1 Mi Piace

Modifica: Inizialmente ho pubblicato la mia domanda su come recuperare gli elementi in base a un campo personalizzato qui, ma ho deciso che la domanda era sufficientemente diversa da meritare un post autonomo. Quindi l’ho pubblicata separatamente qui.

2 Mi Piace

Sto riscontrando un comportamento strano con il composer dopo aver seguito l’esempio dei campi personalizzati per i topic.

Quando faccio clic sul pulsante “crea topic” (ad esempio, nella pagina di visualizzazione della categoria, ma in qualsiasi punto del sito), il composer non si apre e ricevo questo errore:

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

Ho iniziato a vedere questo errore quando ho provato ad aggiungere un nuovo pulsante “crea topic” su una nuova pagina, ma da allora, anche dopo aver rimosso quel nuovo pulsante e qualsiasi codice correlato, l’errore persiste.

In qualche modo, penso che il seguente codice, tratto dall’inizializzatore dei campi personalizzati per i topic, stia causando il problema:

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

Quando rimuovo questo codice, i pulsanti “crea topic” funzionano di nuovo (aprendo correttamente il composer). Quando rimetto il codice, l’errore del composer ricompare.

In precedenza avevo questo codice nel mio plugin senza problemi. Ma ora causa l’errore del composer (anche se ho rimosso qualsiasi codice relativo al composer o ai pulsanti “crea topic” nel mio plugin).

Naturalmente, questo codice è importante: serializza il campo personalizzato. Ma sembra che entri in conflitto con il composer. Avete qualche idea su come risolvere?

Sono riuscito a individuare la causa: stavo cercando di aggiungere due campi personalizzati distinti all’inizializzatore dei campi personalizzati per gli argomenti. Per qualche motivo, questo causava interferenze. Probabilmente esiste un modo corretto per aggiungere due campi personalizzati a quel file, ma il mio codice, che ripeteva la stessa logica per due campi separati, stava creando problemi. Tutto ha funzionato di nuovo regolarmente dopo aver rimosso il secondo campo personalizzato dal file.

Con questo codice scheletro, se volessi aggiungere più campi, ogni campo dovrebbe essere un plugin autonomo?

Sono davvero entusiasta di aver trovato questo tutorial e mi chiedo quanto, se del caso, dovrei modificare questi modelli per farli funzionare per i campi utente personalizzati?

No, devi solo aggiungere codice aggiuntivo per il campo aggiuntivo. Nella maggior parte dei casi, basta duplicare il codice esistente, ad esempio

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

Il primo posto dove cercare i campi utente personalizzati è /admin/customize/user_fields che ti fornisce un’interfaccia utente per aggiungerli. Se vuoi un controllo più granulare, il processo è molto simile a quello degli argomenti e delle categorie, ma in realtà non hai bisogno degli elementi frontend con i campi utente.

In realtà, noi (Pavilion) stiamo pensando di creare un plugin per campi personalizzati (analogo ad ACF per WordPress) che inizialmente assomiglierebbe all’interfaccia di amministrazione dei campi personalizzati nel plugin Custom Wizard.

In realtà, alcune persone utilizzano già il plugin Custom Wizard come gestore di campi personalizzati. Elenca tutti i campi personalizzati sulla tua istanza (da qualsiasi fonte) e ti consente di aggiungere un campo di qualsiasi tipo a qualsiasi modello che li supporti.

Non aggiunge il supporto frontend, ad esempio come quello mostrato nel plugin educativo Topic Custom Field (e questo non funzionerebbe nel contesto del plugin Custom Wizard), motivo per cui stiamo pensando di separarlo in un plugin separato.

3 Mi Piace

@angus, penso che mi piacerebbe molto.

Soprattutto se aggiunge il supporto al frontend.

Mi piacerebbe avere un modo semplice per gli amministratori di aggiungere campi personalizzati a diverse classi, consentire agli utenti di compilarli (ad esempio, in argomenti, post, profili utente) e avere un modo per il frontend di mostrarli.

La cosa principale che non riesco a ottenere con gli attuali campi utente personalizzati è una varietà di tipi di campo. Al momento sono limitati a 4, credo, e mi piacerebbe avere le opzioni disponibili con il plugin Custom Wizards.

Idealmente, voglio costruire una directory utenti abbastanza avanzata, ricercabile/filtrabile/ordinabile, con molti campi personalizzati di molti tipi, sperimenterò con Custom Wizards per vedere se funzionerà per ora e spero che investirete nel plugin Custom Fields.

Grazie!

@angus

Prima di tutto, grazie mille per questo plugin.

Hai per caso un esempio funzionante di questo plugin (campo personalizzato all’argomento) per gestire più campi personalizzati? Sono riuscito ad aggiungere con successo un campo personalizzato e ho apportato un paio di modifiche senza problemi.

Ho provato a duplicare il codice, modificarlo e aggiungere un plugin aggiuntivo, ecc.

Qualcuno ha un repository di codice o un esempio che è disposto a condividere con me? Qualsiasi aiuto sarebbe molto apprezzato.

1 Mi Piace

Ciao @Joe_Stanton,

L’ho fatto un paio di volte e il modo in cui l’ho fatto è stato quello di memorizzare i campi personalizzati in un array con un oggetto con un nome e un tipo per il campo personalizzato.

Ad esempio:

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

Poi scorro i campi per applicare la logica menzionata in questo argomento. Puoi vedere un esempio di come viene utilizzato in un plugin su cui sto lavorando qui. Tuttavia, il codice pertinente è riportato di seguito.

Esempio

Sul lato server:

  # Registrazione dei campi personalizzati
  fields.each do |field|
    # Registra i campi
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

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

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

    # Aggiornamento alla creazione del topic
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Aggiornamento alla modifica del topic
    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

    # Serializza al Topic
    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

    # Precarica i Campi
    add_preloaded_topic_list_custom_field(field[:name])

    # Serializza alla lista dei topic
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

Analogamente sul lato client per serializzare i campi:


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

  // Serializza Campi Personalizzati:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 Mi Piace

Grazie @keegan!

Penso di aver configurato correttamente il file plugin.rb, il ciclo sembra corretto.

Tuttavia, sto avendo difficoltà con il file topic-custom-field-initializer.js. Ecco il mio codice per entrambi i file. Qualche suggerimento per il file initializer.js? Penso di essere molto vicino, quando creo un nuovo argomento, ottengo 1/3 dei campi, il campo listingDetails ma mi mancano ancora isClassifiedListing e 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 Mi Piace

Non l’ho testato, ma credo che il motivo per cui vedi solo 1/3 dei campi sia che sta iterando e registrando una classe di connettore non univoca, sovrascrivendo quella precedente.

In generale, per il lato client, invece di iterare sui campi personalizzati e dichiarare i metodi API, suggerisco di definire componenti per ciascun campo separatamente, o almeno azioni separate, poiché probabilmente dovrai avere una logica diversa associata a ciascun campo.

L’unica parte su cui itererei e dichiarerei è questa:

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

Per il resto dei componenti, è probabilmente meglio creare una logica separata per ciascun caso.

3 Mi Piace

@keegan ha funzionato! grazie mille per tutti i suggerimenti. Non ce l’avrei fatta senza il tuo aiuto.

4 Mi Piace