TL;DR 바로 동작하는 코드로 건너뛰기 여기
+ New Topic 버튼이 있는 페이지를 보고 있을 때, 인스펙터를 통해 HTML을 확인할 수 있습니다.
그렇게 하면 id가 있음을 알 수 있습니다.
HTML 요소의 id는 고유해야 합니다. 즉… 같은 뷰 내에서 두 요소가 동일한 HTML id 속성을 공유할 수 없다는 뜻입니다. 따라서 시작하기에 충분합니다.
Github에서 "create-topic"을 검색하면 다음과 같은 결과가 나옵니다…
Repository search results · GitHub
왼쪽의 필터를 주목하세요.
버튼의 HTML을 추적해야 하므로, 보내는 액션을 추적하려는 이유에서 handlebars를 선택합니다.
따라서 handlebars를 선택하면 다음과 같은 결과가 나옵니다.
Repository search results · GitHub
여기에는 결과물이 하나만 있으므로 운이 좋습니다. 결과물이 더 많다면 목록을 더 좁힐 수 있는 방법들이 있지만, 그것은 이 주제의 범위를 벗어납니다.
자, 그 파일을 확인해 봅시다.
그러면 버튼의 액션이 다음과 같이 설정되어 있음을 볼 수 있습니다.
action=action
음… 별로 도움이 되지 않네요… 자, 이제 어떻게 해야 할까요?
action=action을 보면, 액션이 부모 템플릿에서 컴포넌트로 전달되고 있음을 의미합니다.
그 컴포넌트를 가진 템플릿이 어떤 것인지 확인해 봅시다. Github로 가서 템플릿에서 사용되듯 컴포넌트 이름을 검색합니다. 이 예제에서는 다음과 같은 것을 사용할 것입니다 "{{create-topic-button"
{{COMPONENT_NAME만 추가하고 나머지는 건너뛴 것에 유의하세요. 전달되는 다른 인자들을 알 수 없으므로 일반적인 검색을 원합니다.
다음은 결과입니다
Repository search results · GitHub
두 개의 결과가 나옵니다… 그중 하나는 styleguide 플러그인에 있으므로 단순히 무시합니다. 다른 하나는 core에 있습니다. 자, 그것이 어떻게 보이는지 살펴봅시다
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
}}
아… 가까워지고 있습니다. 이제 버튼의 액션이
action=(action "clickCreateTopicButton")
임을 볼 수 있습니다.
이제 그 액션이 무엇을 하는지 알아내야 합니다. 따라서 액션 이름을 검색합니다. 그리고 이제 그 액션의 정의를 컴포넌트의 js 파일에서 보고 싶으므로 .js 파일로 필터링합니다.
Repository search results · GitHub
다시, 결과물이 하나만 나오므로 그것을 살펴봅시다.
그러면, 액션은 두 가지 중 하나를 수행하는 것 같습니다. 카테고리가 읽기 전용이고 사용자가 이미 초안을 가지고 있지 않다면, 경고를 표시합니다. 그렇지 않으면 createTopic() 메서드를 호출합니다.
우리는 후자에 관심이 있으므로 그것을 살펴봅시다.
그 파일에서 createTopic()을 검색하면(인라인 검색, Github 아님)… 그것에 대한 참조가 하나만 있음을 알 수 있습니다. 어떻게 된 일일까요? 어떻게 이 컴포넌트가 정의되지 않은 메서드를 호출할 수 있을까요?
음, 답은 파일의 위쪽에 있습니다.
import Component from "@ember/component";
import FilterModeMixin from "discourse/mixins/filter-mode";
import NavItem from "discourse/models/nav-item";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Component.extend(FilterModeMixin, {
router: service(),
tagName: "",
// Should be a `readOnly` instead but some themes/plugins still pass
// the `categories` property into this component
@discourseComputed("site.categoriesList")
categories(categoriesList) {
return categoriesList;
},
이것이 무엇을 의미합니까?
여기에 많은 시간을 쓰지는 않겠지만, Ember는 클래스를 사용합니다. 클래스를 재사용 가능한 코드 묶음으로 생각하세요. 위에서 강조된 모든 줄은 다음을 의미합니다:
Ember Component 묶음을 가져와, FilterModeMixin 묶음을 추가하고, 결과물에 몇 가지 메서드를 더 추가하거나 기존 메서드 중 일부를 오버라이드하여 내 애플리케이션을 위한 새로운 Ember 컴포넌트를 생성하게 하세요.
자, 이제 추적하려는 액션으로 돌아가 봅시다.
clickCreateTopicButton() {
if (this.categoryReadOnlyBanner && !this.hasDraft) {
bootbox.alert(this.categoryReadOnlyBanner);
} else {
this.createTopic();
}
},
이것은 this.createTopic()을 호출합니다. 이것은 기본 Ember 컴포넌트 메서드가 아닙니다. 이는 커스텀 Discourse 메서드이므로 FilterModeMixin에서 나와야 합니다. FilterModeMixin이란 무엇일까요? 음… 그것은 파일의 상단에 정의되어 있습니다.
import FilterModeMixin from "discourse/mixins/filter-mode";
그러므로, 거기로 가야 할 것 같습니다.
discourse/app/assets/javascripts/discourse/app/mixins/filter-mode.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub
잠시 멈추고 그 파일에서 createTopic()에 대한 인라인 검색을 해보세요. 진심입니다. 읽기를 멈추고 해보세요. 기다릴게요… 속지 마세요… 저는 당신을 하고 있습니다.
좋아요. 검색했고, 결과가 없었습니다. 이제 어떻게 해야 할까요?
위에서 설명한 것은 값을 전달하는 방법 중 하나일 뿐입니다. 찾고 있는 것을 찾지 못하면. 한 발짝 물러서서 다른 접근 방식을 시도해 보세요.
자, 요약해 봅시다… 지금 우리는 어디에 있나요? 막히기 전에 우리는 d-navigation 컴포넌트의 JS 파일을 보고 있었습니다. 그 템플릿을 살펴봅시다.
다시, "{{COMPONENT_NAME"을 사용하여 검색합니다.
https://github.com/discourse/discourse/search?q="{{d-navigation"
이것은 네 개의 결과를 줍니다…
이것이 중요합니까? 어쩌면요. 이 경우에 중요합니까? 아니요. 우리는 단순히 createTopic()이 어디서 오는지 또는 그것이 무엇인지 알아내려고 하고 있습니다. 따라서 첫 번째 결과로 가 봅시다.
discourse/app/assets/javascripts/discourse/app/templates/navigation/default.hbs at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub
보세요…
createTopic=(route-action "createTopic")
훌륭합니다… 더 많은 전문 용어… 모두가 그것을 좋아하니까
진지하게 말하자면, route action에 대해 이야기해 봅시다. 그것들은 무엇일까요? 음. 그것들은 route…action입니다? 즉, route에 정의된 액션입니다. 그것들이 좋은 이유는 무엇일까요? Discourse에서 route가 중첩될 수 있기 때문입니다.
이렇게 생각해 보세요
- route-1
- route-1-1
- route-1-2
- route-1-3
route 111, 112, 113에서 다른 매개변수로 작동해야 하는 공유 컴포넌트가 있다면, 그것들 모두에서 동일한 컴포넌트를 사용하고 동일한 액션을 전달한 다음 필요에 따라 각 route에 맞게 수정하는 것이 더 쉬울 것 아닙니까?
route-action이 하는 일이 바로 그것입니다.
좋아요, 질문으로 돌아가 봅시다. 우리는 다음을 보고 있었습니다.
createTopic=(route-action "createTopic")
navigation/default 컴포넌트에서.
이제 그 route action이 무엇을 하는지 확인하기 위해 route가 무엇인지 알아내면 됩니다.
/c/cat-slug/cat-id 페이지에서 새 주제 버튼의 동작을 수정하려고 합니다. 따라서 그런 페이지 중 하나를 방문해 봅시다. 예를 들어: http://localhost:4200/c/meta/6
이 route는 무엇일까요? Discourse에 정말 익숙하지 않다면 알 수 없을 것입니다. 자, 이제 어떻게 해야 할까요?
여기서 브라우저용 ember 확장 프로그램이 유용해집니다.
여기에서 설치하세요 아직 없다면. 기다릴게요.
(링크는 Github 저장소이지만, 설명에 다른 브라우저용 확장 프로그램 링크가 있습니다)
좋아요, 이제 설치했으니 /c/cat-slug/cat-id 페이지를 다시 방문하고 확장 프로그램 페이지를 살펴보세요.
로드되면 Routes를 클릭한 다음 "Current Route only`를 토글하세요.
아… 보세요. 이제 우리가 어떤 route에 있는지 알 수 있습니다. 우리는 discovery.category에 있습니다.
하지만 그것이 전부가 아닙니다… 그것은
application > discovery > discovery.category 입니다.
기억하세요, route는 중첩됩니다. 자, 이제 어떻게 해야 할까요?
보통은 맨 위에서 시작합니다. 이 경우, 그것은 application route가 될 것입니다. 그 route의 파일을 찾아서 액션이 거기에서 정의되어 있는지 검색해 보세요.
discourse/app/assets/javascripts/discourse/app/routes/application.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub
결과는 그렇지 않은 것으로 나타납니다… 따라서 우리는 중첩 트리에서 아래로 내려가 discovery route로 갑니다.
discourse/app/assets/javascripts/discourse/app/routes/discovery.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub
거기에서 검색하고… bingo!
좋아요, 이제 우리는
createTopic=(route-action "createTopic")
이 무엇을 가리키는지 알았습니다. 자, 그 액션을 살펴봅시다.
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
this.openComposer(this.controllerFor("discovery/topics"));
}
},
두 가지 중 하나를 수행하는 것 같습니다. 사용자가 초안을 가지고 있다면, 그것을 엽니다. 그렇지 않으면 매개변수와 함께 openComposer()를 호출합니다. 다음은 무엇일까요? 이제 답을 이미 알고 있어야 합니다. 우리는 openComposer()이 어디서 오는지 또는 그것이 무엇을 하는지 알아내야 합니다.
따라서 파일에서 openComposer()을 검색하고… 물론 어떤 결과도 나오지 않습니다. 그 route에는 openComposer()라고 하는 메서드가 없습니다.
다음은 무엇일까요? Ember Classes에 대한 부분을 기억하나요? 그것을 시도해 봅시다.
우리는 route 파일의 상단에 이것을 가지고 있습니다.
The parent route for all discovery routes.
Handles the logic for showing the loading spinners.
**/
import DiscourseRoute from "discourse/routes/discourse";
import OpenComposer from "discourse/mixins/open-composer";
import User from "discourse/models/user";
import { scrollTop } from "discourse/mixins/scroll-top";
import { setTopicList } from "discourse/lib/topic-list-tracker";
import Site from "discourse/models/site";
export default DiscourseRoute.extend(OpenComposer, {
queryParams: {
filter: { refreshModel: true },
},
redirect() {
return this.redirectIfLoginRequired();
},
beforeModel(transition) {
const url = transition.intent.url;
이것은 이 route가 DiscourseRoute "묶음"의 모든 메서드뿐만 아니라 OpenComposer "묶음"에서 정의된 메서드도 상속한다는 것을 의미합니다.
openComposer가 우리가 원하는 것이 더 가능성이 높으므로, 그것을 살펴봅시다. 하지만 그렇게 하기 전에… 우리는 openComposer가 그 파일에서 어떻게 정의되는지 봐야 합니다.
import OpenComposer from "discourse/mixins/open-composer";
URL을 보세요. 그것은 Ember 컴포넌트가 아닙니다. route도 아니고, model도 아닙니다. 그것은 mixin입니다. mixin이란 도대체 무엇일까요? 정말, 정말 짧은 답은… 재사용 가능한 함수들의 묶음입니다.
당신은 mixin에서 이것들을 정의합니다.
add(number) {
return number + 1
}
substract(number) {
return number - 1
}
그런 다음 mixin을 Ember 컴포넌트에 추가하면, 다음과 같은 일을 할 수 있습니다.
// starting value is 1
myMethod () {
this.add(value) // returns 2
this.substract(value) // returns 0
}
자, 이것이 우리가 하려는 일과 어떻게 관련이 있을까요?
음, open-composer 여기요.
import OpenComposer from "discourse/mixins/open-composer";
은 mixin입니다. 그 mixin의 메서드 중 하나는 OpenComposer()입니다.
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,
});
},
이것이 혼란스러울 수 있습니다. 그것들은 동일한 이름을 공유하지만 - 하나는 대문자로 시작하며, 이는 그것이 Class임을 나타냅니다.
그것들은 다른 의미를 가집니다.
이를 이해하려면, import된 모듈에 부여하는 이름은 중요하지 않다는 것을 이해해야 합니다 (이 특정 경우에서), "default"로 export되는 한.
이를 설명하는 것은 이 주제의 범위를 약간 벗어납니다. 당신이 알아야 할 것은 이것입니다.
여기서 OpenComposer
discourse/app/assets/javascripts/discourse/app/routes/discovery.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub
그리고 여기서 openComposer()
discourse/app/assets/javascripts/discourse/app/mixins/open-composer.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub
은 동일한 것이 아닙니다.
좋아요… 요약해 봅시다.
New topic button HTML id < New topic button action < d-navigation component action < discovery-route action < OpenComposer mixin < openComposer() method
자… 이것은 그 route에서 + New Topic 버튼을 클릭할 때 궁극적으로 호출되는 메서드입니다.
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,
});
},
자, 당신의 질문으로 돌아가 봅시다.
우리는 /c/cat-slug/cat-id에서 그 버튼의 액션을 어떻게 알아낼 수 있는지 확립했지만, 당신이 하려는 /tags/category-slug/tag-name을 방문할 때 일어나는 일과는 다르게 보입니다.
자, 다음 단계는 무엇일까요? 그 route가 createTopic() 액션을 처리하기 위해 무엇을 하는지 살펴봅시다.
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-fill the tags input field
if (composerController.canEditTags && controller.get("model.id")) {
const composerModel = this.controllerFor("composer").get("model");
composerModel.set(
"tags",
[
controller.get("model.id"),
This file has been truncated. show original
음… 당신은 그것이 액션을 다르게 처리하는 것을 알아차릴 것입니다.
/c/cat-slug/cat-id의 경우 그것은 다음과 같습니다.
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
this.openComposer(this.controllerFor("discovery/topics"));
}
},
/tags/category-slug/tag-name의 경우 그것은 다음과 같습니다.
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-fill the tags input field
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)
);
}
});
}
}
이 차이는 당신이 여기에서 묻는 것과 거의 동일합니다.
자, 당신이 해야 할 일은… discovery route의 createTopic() 액션을 수정하여 tag-show route에서 작동하는 것처럼 작동하게 하는 것입니다. 자, 어떻게 그것을 합니까?
Ember가 Classes를 사용한다고 우리가 이야기한 것을 기억하나요? 네, 우리는 그것으로 다시 돌아가야 합니다.
플러그인 API는 이 메서드를 통해 Ember classes를 수정할 수 있게 해줍니다.
https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L166-L195
자, 여기서 우리는 무엇을 수정하려고 합니까? discovery route… 왜냐하면… 기억하세요, /c/cat-slug/cat-id와 같은 페이지에 있을 때 createTopic()이 정의되어 있는 곳이 그곳이기 때문입니다.
우리는 이것으로 시작합니다.
api.modifyClass("route:discovery", {
pluginId: "prefill-composer-tags",
actions: {
createTopic() {
console.log("fires");
}
}
});
그것이 무엇을 합니까? 그것은 + New Topic 버튼을 깨뜨립니다; 그러나, 그것이 우리가 올바른 방향에 있음을 알려줍니다. 위의 스니펫을 추가해 보면, 버튼을 클릭해도 더 이상 composer가 열리지 않음을 알아차릴 것입니다. 대신, 그것은 단순히 콘솔에 메시지를 출력합니다. 이것은 좋은 일입니다. 왜냐하면 우리가 올바른 Class와 올바른 액션 - route:discovery과 createTopic()을 표적했음을 의미하기 때문입니다.
자, 다음은 무엇일까요? 음, /tags/category-slug/tag-name의 버튼이 우리가 원하는 것을 정확히 수행한다는 것을 기억하세요. 따라서, 그 route에서 코드를 복사하고 필요한 import를 추가해 봅시다.
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-fill the tags input field
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)
);
}
});
}
}
}
});
그것이 작동할까요? 아니요, 하지만 우리는 한 단계 더 가까워졌습니다. 왜 작동하지 않을까요? 왜냐하면 composer가 열릴 때 추가하는 태그가 정의되어 있지 않기 때문입니다. 왜? 그것들은 tag.show 컨트롤러에서 로드되기 때문입니다 - 그것은 우리가 원하는 것이 아닙니다. 우리가 있는 route와 함께 작동하도록 코드를 수정해 봅시다.
그러나, 그렇게 하기 전에, 우리가 원하는 기본 태그에 대한某种의 인덱스가 필요합니다. 다음과 같은 새로운 객체로 가 봅시다.
// category-slug: [DEFAULT_TAGS_ARRAY]
const defaultTagIndex = {
// single word slug
meta: ["a", "b", "c"],
core: ["g", "h"],
// slug with a dash
["general-chat"]: ["d", "e", "f"]
};
이것은 기본적으로 composer가 meta 카테고리 페이지에서 열리면 태그 "a,b,c"를 추가한다는 것을 의미합니다.
composer가 core 카테고리 페이지에서 열리면 태그 "g,h"를 추가하고, 그와 같이.
이제 그것을 가지고 있으므로, 액션을 다음과 같이 보이도록 수정할 수 있습니다.
최종 코드
const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");
// category-slug: [DEFAULT_TAGS_ARRAY]
const defaultTagIndex = {
// single word slug
meta: ["a", "b", "c"],
core: ["g", "h"],
// slug with a dash
["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-fill the tags input field
if (composerController.canEditTags && categoryId) {
const composerModel = composerController.model;
composerModel.set(
"tags",
makeArray(defaultTagIndex[categorySlug]).filter(Boolean)
);
}
});
}
} catch {
this._super(...arguments);
return;
}
}
}
});
참고:
저는 모든 것을 try…catch 블록으로 감쌌습니다. 코드가 실패하면, 우리는 this._super(...arguments)를 실행합니다.
Ember에 익숙하다면, this._super(...arguments)이 무엇을 하는지 알 것입니다. 그렇지 않다면, 여기 간단한 설명이 있습니다. 우리는 createTopic()을 오버라이드하고 있으므로, 오버라이드가 오류로 인해 실패하면 - 아마도 core가 업데이트되었을 수 있습니다 - core에서 정의된 여기 의 메서드로 폴백합니다.
사용자가 새 주제 초안을 가지고 있다면, 우리는 단순히 this._super(...arguments)로 폴백하고 core가 그 일을 하도록 둡니다.
그것이면 충분해야 합니다. 당신이 이제 추가해야 하는 것은 테마 설정을 통해 기본 태그 인덱스를 생성하는 방법뿐입니다.