Sugestões de Edição do Discourse

:writing_hand: Permite que membros da comunidade sugiram edições em publicações, dando aos revisores controle granular sobre quais alterações aceitar — sem conceder permissões completas de edição.

:warning: O plugin é experimental e está sujeito a muitas alterações no momento, ainda não é para uso em produção.

:link: GitHub - discourse/discourse-suggested-edits: EXPERIMENTAL suggested edits plugin · GitHub

Instalação

Siga o guia padrão de instalação de plugins usando o URL do repositório:

https://github.com/discourse/discourse-suggested-edits.git

Por que Edições Sugeridas?

Muitas comunidades querem que os membros ajudem a manter o conteúdo preciso e atualizado, mas conceder acesso de edição a todos nem sempre é prático. Edições Sugeridas preenche essa lacuna — os membros podem propor melhorias nas publicações, e revisores confiáveis decidem o que será aplicado. Pense nisso como trazer um modelo de contribuição estilo Wikipédia para sua comunidade Discourse.

Isso é especialmente útil para:

  • Categorias de base de conhecimento e documentação onde a precisão é importante e muitos olhos ajudam
  • Comunidades com membros mais novos que têm boas contribuições, mas ainda não conquistaram a confiança total de edição
  • Conteúdo colaborativo como FAQs, guias ou referências mantidas pela comunidade
  • Edições automatizadas às vezes sistemas de IA podem sugerir correções de erros de digitação e de voz, e você precisa de um humano no circuito aprovando

Como Funciona

Sugerindo uma edição

Membros no grupo de sugestão configurado veem um botão “Sugerir Edição” em publicações elegíveis. Clicar nele abre o composer pré-preenchido com o conteúdo da publicação. Eles fazem suas alterações, opcionalmente adicionam um motivo e enviam.

image

Revisando sugestões

Revisores veem um distintivo de contagem em publicações com sugestões pendentes. Clicar em “Revisar” abre um modal que divide a sugestão em alterações individuais — cada uma mostrada como um diff destacado com contexto ao redor.

Revisores podem:

  • Aceitar ou rejeitar cada alteração de forma independente — sem precisar aceitar tudo ou nada
  • Editar o texto sugerido antes de aplicar — ajustar a redação preservando a intenção
  • Alternar entre visualizações de diff embutido e lado a lado
  • Navegar entre múltiplas sugestões se mais de uma estiver pendente

Aplicando alterações

Quando o revisor clica em “Aplicar Aceitas”, as alterações selecionadas são aplicadas à publicação como uma revisão atribuída ao autor da sugestão, com um motivo de edição notando quem a aprovou. O autor da sugestão e quaisquer outros usuários afetados são notificados.

Tratamento de obsolescência

Se a publicação original for editada após a criação de uma sugestão, a sugestão é automaticamente marcada como obsoleta e não pode ser aplicada. Isso evita conflitos e garante que as sugestões sejam sempre baseadas no conteúdo atual. Os autores das sugestões são notificados para que possam reenviar, se necessário.

Configuração

Habilite o plugin e configure o acesso em Admin > Configurações, pesquisando por “suggested edits”:

Configuração Descrição
suggested_edits_enabled Alternância mestre para o plugin
suggested_edits_suggest_groups Grupos cujos membros podem sugerir edições
suggested_edits_review_groups Grupos cujos membros podem revisar e aplicar sugestões. Os autores das publicações podem sempre revisar sugestões em suas próprias publicações.
suggested_edits_included_categories Categorias onde as edições sugeridas estão habilitadas
suggested_edits_included_tags Tags nos tópicos onde as edições sugeridas estão habilitadas
suggested_edits_max_creates_per_minute Limite de taxa para criação de sugestões (padrão: 5)
suggested_edits_max_revisions_per_minute Limite de taxa para revisão de sugestões (padrão: 10)

Uma configuração típica

  1. Habilite o plugin
  2. Defina grupos de sugestão para o nível de confiança ou grupo que deve poder propor edições (ex: trust_level_1)
  3. Defina grupos de revisão para seus moderadores ou curadores (ex: staff)
  4. Escolha as categorias ou tags onde você quer que isso seja habilitado — você não precisa ativar em todos os lugares

:bulb: Os autores das publicações podem sempre revisar sugestões em suas próprias publicações, independentemente da configuração do grupo de revisão.

Escopo e Limitações

  • Apenas primeiras publicações — As edições sugeridas atualmente se aplicam às primeiras publicações de tópicos (OPs), não a respostas
  • Uma sugestão pendente por usuário por publicação — Um membro deve esperar que sua sugestão atual seja resolvida antes de enviar outra na mesma publicação
  • Sugestões baseadas em texto — O diff é calculado no conteúdo Markdown bruto da publicação

Pesquisa

Revisores podem usar o filtro de pesquisa with:suggested-edits para encontrar tópicos com sugestões pendentes em todo o fórum.

17 curtidas

Acho que eu não fui notificado.

3 curtidas

Hmm… o OP do tópico é notificado quando há uma sugestão de edição em sua postagem? Procurei pelo código, mas não encontrei nenhuma indicação de que isso aconteça.

2 curtidas

Não, ainda não foi implementado. Vou adicionar.

2 curtidas

Olá, ótima ideia! Ao escolher as categorias, é possível selecionar apenas as categorias de primeiro nível e as subcategorias seriam selecionadas automaticamente? Obrigado.

1 curtida

FYI este plugin está gerando um erro:

plugin-api.gjs:234 [PLUGIN discourse-suggested-edits] Tentativa de modificar "service:composer", mas ele já foi inicializado anteriormente no processo de inicialização (por exemplo, via um lookup()). Remova esse lookup ou mova a chamada modifyClass para uma etapa anterior no processo de inicialização para que as alterações surtam efeito. https://meta.discourse.org/t/262064
_resolveClass @ plugin-api.gjs:234

Parece que o inicializador está chamando api.customizeComposerText() logo antes de api.modifyClass("service:composer", {...}). Acredito que o código precise ser reordenado para que api.modifyClass seja executado antes de api.customizeComposerText:

talvez devesse ficar assim?
  // modifique a classe do compositor primeiro

  api.modifyClass("service:composer", {
    pluginId: "discourse-suggested-edits",

    async open(opts) {
      if (isSuggestedEditAction(opts.action) && this.model) {
        this.model.set("disableDrafts", true);
        this.skipAutoSave = true;
        this.close();
        this.skipAutoSave = false;
      }
      setSuggestEditActive(
        isSuggestedEditAction(opts.action),
        opts.metaData?.originalRaw
      );
      await this._super(opts);
      if (isSuggestedEditModel(this.model)) {
        this.model.setProperties({
          disableDrafts: true,
          draftStatus: null,
          draftConflictUser: null,
        });
      }
    },

    cancelComposer(opts) {
      if (isSuggestedEditModel(this.model)) {
        setSuggestEditActive(false);
        this.skipAutoSave = true;
        this.close();
        this.appEvents.trigger("composer:cancelled");
        this.skipAutoSave = false;
        return Promise.resolve();
      }
      return this._super(opts);
    },

    destroyDraft() {
      if (isSuggestedEditModel(this.model)) {
        return Promise.resolve();
      }
      return this._super();
    },

    _saveDraft(showToast = false) {
      if (isSuggestedEditModel(this.model)) {
        return Promise.resolve();
      }

      return this._super(showToast);
    },

    save(force, options = {}) {
      if (isSuggestedEditModel(this.model)) {
        return this._saveSuggestedEdit();
      }
      return this._super(force, options);
    },

    async _saveSuggestedEdit() {
      const model = this.model;
      const meta = model.metaData || {};

      if (model.reply?.trim() === meta.originalRaw?.trim()) {
        this.toasts.error({
          data: {
            message: i18n("discourse_suggested_edits.composer.no_changes"),
          },
          duration: "short",
        });
        return;
      }

      try {
        if (meta.existingSuggestionId) {
          await updateSuggestedEdit(meta.existingSuggestionId, {
            raw: model.reply,
            reason: meta.reason,
          });
          this.toasts.success({
            data: {
              message: i18n("discourse_suggested_edits.toast.updated"),
            },
            duration: "short",
          });
        } else {
          const result = await createSuggestedEdit({
            postId: meta.postId,
            raw: model.reply,
            reason: meta.reason,
          });
          if (model.topic) {
            model.topic.set(
              "own_pending_suggested_edit_id",
              result.suggested_edit.id
            );
          }
          this.toasts.success({
            data: {
              message: i18n("discourse_suggested_edits.toast.created"),
            },
            duration: "short",
          });
        }

        setSuggestEditActive(false);
        this.close();
      } catch (e) {
        popupAjaxError(e);
      }
    },
  });

  // adicione as customizações após a classe ter sido modificada

  api.customizeComposerText({
    actionTitle(model) {
      if (model.action === SUGGEST_EDIT_ACTION) {
        return i18n("discourse_suggested_edits.composer.action_title");
      }
    },
    saveLabel(model) {
      if (model.action === SUGGEST_EDIT_ACTION) {
        return "discourse_suggested_edits.composer.save_label";
      }
    },
  });
1 curtida

Podemos precisar mover isso para um pré-inicializador… vou dar uma olhada, obrigado por tentar @Lilly

1 curtida