Компонент GJS завершается с ошибкой «undefined helper» при отображении через сервис модального окна (Discourse 3.5.0)

При попытке отобразить компонент Glimmer (.gjs) в виде модального окна с помощью this.modal.show() возникает постоянная ошибка рендеринга. Модальное окно вызывается из другого компонента GJS, добавленного в меню постов через трансформер значений post-menu-buttons. Я работаю на версии Discourse v3.5.0.beta3-dev.

Я пытаюсь добавить кнопку в меню постов, используя api.registerValueTransformer("post-menu-buttons", ...). При нажатии на эту кнопку должно открываться модальное окно, определенное отдельным компонентом GJS (FeedbackFormModal), с помощью вызова this.modal.show(FeedbackFormModal, ...).

Когда кнопка нажата и вызывается this.modal.show(), приложение падает с следующей ошибкой, по-видимому, в процессе рендеринга компонента FeedbackFormModal:

Произошла ошибка:

- Во время рендеринга:
  - корневой уровень
    application
      (неизвестный компонент только с шаблоном)
        DiscourseRoot
          ModalContainer
            FeedbackFormModal
              DModal
                conditional-in-element:ConditionalInElement
                  (неизвестный компонент только с шаблоном) index.js:3970:18

Произошла ошибка: index.js:3377:16

Uncaught (in promise) Error: Попытка использовать значение как хелпер, но оно не является объектом или функцией. Определения хелперов должны быть объектами или функциями с ассоциированным менеджером хелперов. Значение было: undefined
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:89256
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90066
    Ember 4
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90164
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:89224
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90163
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90284
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:92117
    Ember 12
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:94577
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:96288
    Ember 34
    show modal.js:73
    openFeedbackModal leave-feedback-button.js:102 // Номер строки может немного отличаться
    _triggerAction d-button.gjs:138
    Ember 10

Это происходит даже тогда, когда шаблон FeedbackFormModal сведен к абсолютному минимуму и содержит только импортированные основные компоненты (<DModal>, <DButton>) и стандартные встроенные хелперы (if, on и т.д.).

Ниже приведен код для справки:

plugin.rb

# frozen_string_literal: true
# name: my-plugin

module ::MyPlugin
  # ... константы ...
end

require_relative "lib/my_plugin/engine"

after_initialize do
  # Загрузка зависимостей (с использованием require_dependency и File.expand_path)
  require_dependency File.expand_path('app/controllers/my_plugin/my_controller.rb', __dir__)
  require_dependency File.expand_path('app/models/my_plugin/my_model.rb', __dir__)
  # ... другие зависимости ...

  # Добавление методов к User (с использованием class_eval, так как prepend не сработал)
  ::User.class_eval do
    # Определение вспомогательных методов, таких как my_custom_stat и т.д.
    def my_custom_stat; # ... реализация ...; end
    public :my_custom_stat
    # ... другие методы ...
  end

  # Предварительное добавление расширений Guardian (если есть)
  # ::Guardian.prepend(MyPlugin::GuardianExtensions)

  # Изменения сериализаторов
  reloadable_patch do |plugin|
    # Добавление атрибутов к сериализаторам, например:
    add_to_serializer(:post, :some_flag_for_button) do
        # Логика определения, должна ли отображаться кнопка
        true # Пример
    end
    # ... другие добавления к сериализаторам ...
  end
end

assets/javascripts/discourse/initializers/my-plugin-outlets.js

import { apiInitializer } from "discourse/lib/api";
import { hbs } from "ember-cli-htmlbars";
import LeaveFeedbackButton from "../components/leave-feedback-button"; // Компонент кнопки
// ... импорт других компонентов для других слотов ...

export default apiInitializer("1.13.0", (api) => {
  // Использование трансформера значений для кнопки в меню постов
  api.registerValueTransformer("post-menu-buttons", ({ value: dag, context }) => {
    const { post } = context;
    // Логика определения, должен ли рендериться кнопка на основе post.some_flag_for_button
    const shouldRenderButton = post?.some_flag_for_button; // Пример флага

    if (shouldRenderButton) {
      dag.add("leaveMyPluginFeedback", LeaveFeedbackButton, {
        after: "like",
        args: { post: post },
      });
    }
    return dag;
  });

  // ... renderInOutlet для других элементов интерфейса ...
});

Компонент кнопки (assets/javascripts/discourse/components/leave-feedback-button.gjs)

import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import FeedbackFormModal from "./feedback-form-modal"; // Компонент модального окна

export default class LeaveFeedbackButton extends Component {
  @service modal;
  @service appEvents; // Используется для отображения ошибок

  // Аргументы: post

  get buttonLabel() { return "Action Button"; } // Закодировано
  get buttonTitle() { return "Perform Action"; } // Закодировано

  @action
  openFeedbackModal() {
    console.log("Открытие модального окна...");
    try {
       // Упрощенная модель для тестирования
       const modelData = { post_id: this.args.post.id };
       this.modal.show(FeedbackFormModal, { model: modelData });
    } catch(e) {
        console.error("Ошибка при показе модального окна", e);
        this.appEvents.trigger("show:error", "Ошибка открытия модального окна.");
    }
  }

  <template>
    <DButton
      class="btn-default my-plugin-btn"
      @action={{this.openFeedbackModal}}
      @icon="star" {{!-- Пример иконки --}}
      @label={{this.buttonLabel}}
      title={{this.buttonTitle}}
    />
  </template>
}

Компонент модального окна (assets/javascripts/discourse/components/feedback-form-modal.gjs - Сверхупрощенный)

import Component from "@glimmer/component"; // Убедитесь, что это импортировано
// Удалены tracked, action, service и т.д., если не нужны для упрощенной версии
import DModal from "discourse/components/d-modal"; // Убедитесь, что это импортировано
import DButton from "discourse/components/d-button"; // Убедитесь, что это импортировано
import { on, preventDefault } from '@ember/modifier'; // Импортируйте встроенные, если используются

export default class FeedbackFormModal extends Component {
  // Минимальный JS, необходимый для упрощенного шаблона

  // Пример геттера, необходимого шаблону
  get modalTitle() { return "My Modal Title"; } // Закодировано
  get cancelLabel() { return "Cancel"; }
  get submitLabel() { return "Submit"; }

  // Фейковое действие, если нужно кнопке
  @action submitFeedback() { console.log("Фейковая отправка"); }

  {{!-- СВЕРХУПРОЩЕННЫЙ ШАБЛОН, КОТОРЫЙ ВСЕ ЕЩЕ ВЫЗЫВАЕТ ОШИБКУ --}}
  <template>
    <DModal @title={{this.modalTitle}} @closeModal={{@closeModal}} class="feedback-form-modal">
      <:body>
          <p>--- ТЕСТ МИНИМАЛЬНОГО МОДАЛЬНОГО ОКНА ---</p>
          {{#if this.errorMessage}} {{!-- Использование встроенного 'if' --}}
              <div class="alert alert-error" role="alert">{{this.errorMessage}}</div>
          {{/if}}
      </:body>
      <:footer>
          <DButton @action={{@closeModal}} class="btn-flat"> {{this.cancelLabel}} </DButton>
          <DButton @action={{this.submitFeedback}} class="btn-primary" @icon={{if this.isSubmitting "spinner"}}> {{!-- Использование встроенного 'if' --}}
              {{this.submitLabel}}
          </DButton>
      </:footer>
    </DModal>
  </template>
}

Учитывая, что ошибка Attempted to use a value as a helper... undefined сохраняется даже при рендеринге сверхупрощенного шаблона компонента GJS (содержащего только импортированные основные компоненты, такие как <DModal>/<DButton>, и встроенные хелперы, такие как if) через this.modal.show(), вызываемый из компонента, добавленного через registerValueTransformer("post-menu-buttons", ...), что может быть причиной этого?

Существует ли известная проблема или ограничение в разрешении области видимости хелперов/компонентов в модальных окнах, вызываемых таким образом, в последних версиях Discourse (а именно 3.5.0.beta3-dev)? Существуют ли альтернативные рекомендуемые паттерны для отображения модальных форм из кнопок меню постов в GJS?

Любые подсказки будут очень признательны!

Я опробовал ваш минимальный тестовый код, и у меня он работает:

Единственные изменения, которые я внес, — это добавление отсутствующего импорта action, удаление комментария вне шаблона и принудительное отображение кнопки.

Не могли бы вы быстро создать задачу (TC) на GitHub, которая воспроизведёт эту ошибку?

Спасибо, что уделили время. После вашего подтверждения я попытался настроить TC на GitHub с помощью свежего плагина и в итоге сам изолировал проблему.

Она возникает при использовании хелпера if внутри аргумента компонента (@icon={{if...}}) в шаблоне модального окна. Удаление этого конкретного использования позволяет модальному окну корректно отображаться.

Вот что я пытался сделать: @icon={{if this.isSubmitting "spinner"}}

Кстати, мне очень нравится новая тема Horizon.

Здравствуйте,

Продолжаю обсуждение этой проблемы: я создал плагин minimal-modal-test, как и предлагалось.

  • Успех: Когда компонент модального окна тестового плагина (minimal-modal.gjs) содержал только базовые элементы (например, <p>, <textarea>, стандартные <DModal>, <DButton> и даже встроенные хелперы, такие как #if для условных блоков или if для иконки кнопки отправки), он корректно отображался без ошибок при открытии через modal.show(), вызванном из компонента кнопки, добавленного с помощью трансформера значений post-menu-buttons. Это подтверждает, что базовый сервис модальных окон, рендеринг компонентов, трансформер значений и встроенные хелперы работают корректно в изоляции в моей среде (Discourse v3.5.0.beta3-dev).

  • Неудача: Ошибка (TypeError: userProvidedCallback is undefined / ранее Attempted to use a value as a helper... undefined) возвращается постоянно в тот момент, когда я снова добавляю дочерний компонент <StarRatingInput> в шаблон модального окна (minimal-modal.gjs или feedback-form-modal.gjs).

Я убедился, что сам компонент StarRatingInput использует ранее выявленные обходные пути (inline SVG вместо хелпера dIcon, стрелочные функции для методов, таких как starClass, чтобы исправить контекст this, стандартный JS Array.from вместо хелпера range). Внутренний код StarRatingInput теперь, кажется, корректен.

Это указывает на то, что ошибка вызывается рендерингом вложенного компонента <StarRatingInput> (который содержит цикл #each, динамические привязки классов и обработчики событий) внутри родительского компонента модального окна в этом конкретном контексте рендеринга.

https://github.com/rahul-rakesh/discourse-modal-render-test

Шаги для воспроизведения

  1. У вас запущена последняя версия среды разработки Discourse.

  2. Войдите в систему под любым пользователем.

  3. Перейдите к любой теме и откройте первый пост.

  4. Нажмите кнопку “Test Failing Modal”, добавленную в меню поста.

  5. Откройте консоль разработчика в браузере.

Ожидаемый результат

Должно появиться простое модальное окно, содержащее поле ввода рейтинга звезд.

Фактический результат

Модальное окно не отображается полностью. В консоли браузера отображается ошибка.

Спасибо за репозиторий, он помог!
Я нашел вашу проблему. Дело не в компоненте StartRatInput, а в том, что вы импортируете eq из неверного пути.

Ошибка возникает здесь:

checked={{eq this.testRating ratingValue}}

Вам нужно изменить импорт:

import { eq } from "@ember/helper";

import { eq } from "truth-helpers";

Результат:

Спасибо вам огромное за это! Половину времени я тратила впустую, мучаясь с импортом. :roll_eyes: