모달 서비스를 통해 표시할 때 GJS 구성 요소가 "undefined helper" 오류로 실패합니다 (Discourse 3.5.0)

Glimmer 컴포넌트(.gjs)를 this.modal.show()를 사용하여 모달로 표시하려고 할 때 지속되는 렌더링 오류를 겪고 있습니다. 이 모달은 post-menu-buttons 값 변환기를 통해 게시글 메뉴에 추가된 다른 GJS 컴포넌트에서 트리거됩니다. 현재 Discourse v3.5.0.beta3-dev 버전을 사용 중입니다.

api.registerValueTransformer("post-menu-buttons", ...)를 사용하여 게시글 메뉴에 버튼을 추가하려고 합니다. 이 버튼을 클릭하면 this.modal.show(FeedbackFormModal, ...)를 사용하여 별도의 GJS 컴포넌트(FeedbackFormModal)로 정의된 모달이 열려야 합니다.

버튼이 클릭되고 this.modal.show()가 호출되면, FeedbackFormModal의 렌더링 과정 중 다음과 같은 오류로 인해 애플리케이션이 충돌합니다.

Error occurred:

- While rendering:
  -top-level
    application
      (unknown template-only component)
        DiscourseRoot
          ModalContainer
            FeedbackFormModal
              DModal
                conditional-in-element:ConditionalInElement
                  (unknown template-only component) index.js:3970:18

Error occurred: index.js:3377:16

Uncaught (in promise) Error: Attempted to use a value as a helper, but it was not an object or function. Helper definitions must be objects or functions with an associated helper manager. The value was: 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 // Line number might differ slightly
    _triggerAction d-button.gjs:138
    Ember 10

FeedbackFormModal 템플릿을 임포트된 핵심 컴포넌트(<DModal>, <DButton>)와 표준 내장 헬퍼(if, on 등)만 포함하도록 최소한으로 줄여도 이 오류가 발생합니다.

참고를 위해 아래 코드를 첨부합니다:

plugin.rb

# frozen_string_literal: true
# name: my-plugin

module ::MyPlugin
  # ... constants ...
end

require_relative "lib/my_plugin/engine"

after_initialize do
  # Load Dependencies (using require_dependency and 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__)
  # ... other dependencies ...

  # Add methods to User (using class_eval as prepend failed)
  ::User.class_eval do
    # Define helper methods like my_custom_stat, etc.
    def my_custom_stat; # ... implementation ...; end
    public :my_custom_stat
    # ... other methods ...
  end

  # Prepend Guardian Extensions (if any)
  # ::Guardian.prepend(MyPlugin::GuardianExtensions)

  # Serializer modifications
  reloadable_patch do |plugin|
    # Add attributes to serializers, e.g.:
    add_to_serializer(:post, :some_flag_for_button) do
        # Logic to determine if button should show
        true # Example
    end
    # ... other serializer additions ...
  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"; // Button component
// ... import other components for other outlets ...

export default apiInitializer("1.13.0", (api) => {
  // Use Value Transformer for Post Menu Button
  api.registerValueTransformer("post-menu-buttons", ({ value: dag, context }) => {
    const { post } = context;
    // Logic to determine if button should render based on post.some_flag_for_button
    const shouldRenderButton = post?.some_flag_for_button; // Example flag

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

  // ... renderInOutlet for other UI elements ...
});

버튼 컴포넌트 (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"; // The modal component

export default class LeaveFeedbackButton extends Component {
  @service modal;
  @service appEvents; // Used for error display

  // Args: post

  get buttonLabel() { return "Action Button"; } // Hardcoded
  get buttonTitle() { return "Perform Action"; } // Hardcoded

  @action
  openFeedbackModal() {
    console.log("Opening Modal...");
    try {
       // Simplified model for testing
       const modelData = { post_id: this.args.post.id };
       this.modal.show(FeedbackFormModal, { model: modelData });
    } catch(e) {
        console.error("Error showing modal", e);
        this.appEvents.trigger("show:error", "Error opening modal.");
    }
  }

  <template>
    <DButton
      class="btn-default my-plugin-btn"
      @action={{this.openFeedbackModal}}
      @icon="star" {{!-- Example icon --}}
      @label={{this.buttonLabel}}
      title={{this.buttonTitle}}
    />
  </template>
}

모달 컴포넌트 (assets/javascripts/discourse/components/feedback-form-modal.gjs - 최소 단순화)

import Component from "@glimmer/component"; // Ensure this is imported
// Removed tracked, action, service etc. if not needed by simplified version
import DModal from "discourse/components/d-modal"; // Ensure this is imported
import DButton from "discourse/components/d-button"; // Ensure this is imported
import { on, preventDefault } from '@ember/modifier'; // Import built-ins if used

export default class FeedbackFormModal extends Component {
  // Minimal JS needed for simplified template

  // Example getter needed by template
  get modalTitle() { return "My Modal Title"; } // Hardcoded
  get cancelLabel() { return "Cancel"; }
  get submitLabel() { return "Submit"; }

  // Dummy action if needed by button
  @action submitFeedback() { console.log("Dummy submit"); }

  {{!-- ULTRA-SIMPLIFIED TEMPLATE THAT STILL CAUSES ERROR --}}
  <template>
    <DModal @title={{this.modalTitle}} @closeModal={{@closeModal}} class="feedback-form-modal">
      <:body>
          <p>--- MINIMAL MODAL TEST ---</p>
          {{#if this.errorMessage}} {{!-- Using built-in '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"}}> {{!-- Using built-in 'if' --}}
              {{this.submitLabel}}
          </DButton>
      </:footer>
    </DModal>
  </template>
}

registerValueTransformer("post-menu-buttons", ...)를 통해 추가된 컴포넌트에서 트리거되는 this.modal.show()를 통해 초간단 GJS 컴포넌트 템플릿(임포트된 핵심 컴포넌트(<DModal>/<DButton>)와 if와 같은 내장 헬퍼만 포함)를 렌더링할 때도 Attempted to use a value as a helper... undefined 오류가 지속되는 점을 고려할 때, 이 문제의 원인은 무엇일 수 있을까요?

최근 Discourse 버전(특히 3.5.0.beta3-dev)에서 이 방식으로 트리거되는 모달의 헬퍼/컴포넌트 스코프 해석에 알려진 문제나 제한 사항이 있나요? GJS에서 게시글 메뉴 버튼으로 모달 폼을 표시하기 위한 권장되는 대안 패턴이 있을까요?

도움을 주시면 감사하겠습니다!

질문자의 최소 테스트 코드를 시도해 봤는데, 제 환경에서는 정상적으로 동작합니다:

제가 변경한 내용은 누락된 action 임포트를 추가하고, 템플릿 바깥에 있는 주석을 제거하며, 버튼이 표시되도록 강제하는 것이었습니다.

이 오류를 재현할 수 있는 빠른 TC(테스트 케이스)를 GitHub에서 만들어 주실 수 있으신가요?

확인해 주셔서 감사합니다. 확인을 받은 후 새 플러그인으로 GitHub TC를 설정해보려다가 결국 문제를 직접 분리해냈습니다.

모달 템플릿 내부의 컴포넌트 인자(@icon={{if...}})에서 if 헬퍼를 사용할 때 발생했습니다. 이 특정 사용법을 제거하면 모달이 정상적으로 렌더링됩니다.

제가 하려던 것은 @icon={{if this.isSubmitting "spinner"}} 이었습니다.

참고로 이 새로운 Horizon 테마가 정말 마음에 듭니다.

안녕하세요,

이 이슈에 대해 후속 보고드립니다: 제안하신 대로 minimal-modal-test 플러그인을 생성했습니다.

  • 성공: 테스트 플러그인의 모달 컴포넌트(minimal-modal.gjs)에 기본 요소(<p>, <textarea>, 핵심 <DModal>, <DButton>, 그리고 조건 블록을 위한 #if 내장 헬퍼 또는 제출 버튼 아이콘에 사용된 if 등)만 포함되었을 때, post-menu-buttons 값 변환기를 통해 추가된 버튼 컴포넌트에서 트리거된 modal.show()를 통해 열리면 에러 없이 올바르게 렌더링되었습니다. 이는 제 환경(Discourse v3.5.0.beta3-dev)에서 기본 모달 서비스, 컴포넌트 렌더링, 값 변환기 및 내장 헬퍼가 독립적으로 기능하는 것을 확인해 줍니다.

  • 실패: 자식 컴포넌트 <StarRatingInput>을 모달 템플릿(minimal-modal.gjs 또는 feedback-form-modal.gjs)에 다시 추가하는 순간, 에러(TypeError: userProvidedCallback is undefined / 이전에는 Attempted to use a value as a helper... undefined)가 일관되게 재현됩니다.

StarRatingInput 컴포넌트 자체는 이전에 식별된 우회 방안(인라인 SVG 사용, this 컨텍스트 수정을 위한 starClass와 같은 메서드에 화살표 함수 사용, range 헬퍼 대신 표준 JS Array.from 사용)을 적용하도록 확인했습니다. 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)을 변경해야 합니다:

import { eq } from "@ember/helper";
→
import { eq } from "truth-helpers";

결과:

이렇게 도와주셔서 정말 감사합니다! 절반의 시간을 import 문제로 씨름하면서 낭비했거든요. :roll_eyes: