mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 17:53:55 +08:00
This creates a new API method, `addComposerAction`, which is used to add
a new "Create Event" action that's available in event category types
(enabled by the upcoming change `events category type setup`).
When creating a new topic in an events category, the composer will use
the "Create Event" action by default which automatically enters the
BBCode to create a new event.
Frontend core changes:
* `composer-actions.js` - exports `registerComposerAction` and
`_clearRegisteredActions`, merges the actions into the dropdown, and the
selection routes to `opts.action()`
* `models/composer.js` - `titlePlaceholder` checks
`customizationFor("titlePlaceholder")` to apply a custom title
placeholder based on category
* `services/composer.js` - `saveIcon` checks
`customizationFor("saveIcon")` which is also based on category
Backend core changes:
* `category_types` is moved from `CategorySerializer` to
`SiteCategorySerializer` because the composer gets category info from
`Site.categories`
* `Categories::Configure` and `Categories::Unconfigure` run
`Site.clear_cache` so category type changes happen immediately, they
were being cached for 30 minutes otherwise
Calendar and events plugin:
* Initializer adds the event action type
* Inserts BBCode placeholder for events when selecting the event action
or switching to the events category in the composer (default action
type)
* Observes the content of the topic so we can switch action type when an
event is added or removed
The one piece I've saved for a later change is changing the action icon
in the composer... it's a bit wonky to do this in select-kit right now
(requires an observer as far as I can tell because it's not tracked) and
there's a PR that's switching the menu to DMenu
(https://github.com/discourse/discourse/pull/38068), which makes it
significantly easier
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/273d1ab9-e319-4a8f-91d5-d1c0227f3e6a"
/>
337 lines
11 KiB
JavaScript
Vendored
337 lines
11 KiB
JavaScript
Vendored
import { click, fillIn, visit } from "@ember/test-helpers";
|
|
import { test } from "qunit";
|
|
import Site from "discourse/models/site";
|
|
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
|
import { i18n } from "discourse-i18n";
|
|
|
|
const _mutatedCategoryIds = new Set();
|
|
|
|
function markCategoryAsEvents(categoryId) {
|
|
const category = Site.current().categories.find((c) => c.id === categoryId);
|
|
category.categoryTypes = {
|
|
events: { id: "events", name: "Events", configuration_schema: {} },
|
|
};
|
|
_mutatedCategoryIds.add(categoryId);
|
|
}
|
|
|
|
acceptance("Create event composer action", function (needs) {
|
|
needs.user({ can_create_discourse_post_event: true });
|
|
needs.settings({
|
|
calendar_enabled: true,
|
|
discourse_post_event_enabled: true,
|
|
discourse_post_event_allowed_on_groups: "",
|
|
discourse_post_event_allowed_custom_fields: "",
|
|
});
|
|
|
|
needs.hooks.afterEach(function () {
|
|
_mutatedCategoryIds.forEach((id) => {
|
|
const category = Site.current().categories.find((c) => c.id === id);
|
|
if (category) {
|
|
category.categoryTypes = {};
|
|
}
|
|
});
|
|
_mutatedCategoryIds.clear();
|
|
});
|
|
|
|
test("entering an events-type category puts the composer in event mode", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.hasValue(
|
|
/^\[event start="[^"]+" end="[^"]+" status="public" timezone="[^"]+" reminders="[^"]+"\]\n\[\/event\]$/,
|
|
"event bbcode skeleton is inserted"
|
|
);
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("discourse_post_event.composer.create_event_button"));
|
|
assert
|
|
.dom(".save-or-cancel button.create .d-icon-calendar-day")
|
|
.exists("submit button shows the calendar icon in event mode");
|
|
assert
|
|
.dom("#reply-title")
|
|
.hasAttribute(
|
|
"placeholder",
|
|
i18n("discourse_post_event.composer.event_title_placeholder")
|
|
);
|
|
});
|
|
|
|
test("non-events categories leave the composer in topic mode", async function (assert) {
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(1);
|
|
|
|
assert.dom(".d-editor-input").hasNoValue();
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"));
|
|
});
|
|
|
|
test("dropdown shows Create topic in event mode and Create event in topic mode (events-type only)", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
const composerActions = selectKit(".composer-actions");
|
|
|
|
// Non-events category — neither plugin option appears
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(1);
|
|
await composerActions.expand();
|
|
assert
|
|
.dom(`.composer-actions .select-kit-row[data-value="create_event"]`)
|
|
.doesNotExist();
|
|
assert
|
|
.dom(
|
|
`.composer-actions .select-kit-row[data-value="create_regular_topic"]`
|
|
)
|
|
.doesNotExist();
|
|
await composerActions.collapse();
|
|
|
|
// Events-type category — auto-enters event mode; Create topic switch-back appears
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
await composerActions.expand();
|
|
assert
|
|
.dom(`.composer-actions .select-kit-row[data-value="create_event"]`)
|
|
.doesNotExist();
|
|
assert
|
|
.dom(
|
|
`.composer-actions .select-kit-row[data-value="create_regular_topic"]`
|
|
)
|
|
.exists();
|
|
|
|
// Picking Create topic exits event mode and surfaces Create event
|
|
await composerActions.selectRowByValue("create_regular_topic");
|
|
assert.dom(".d-editor-input").hasNoValue("skeleton is cleared on exit");
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"));
|
|
|
|
await composerActions.expand();
|
|
assert
|
|
.dom(`.composer-actions .select-kit-row[data-value="create_event"]`)
|
|
.exists();
|
|
assert
|
|
.dom(
|
|
`.composer-actions .select-kit-row[data-value="create_regular_topic"]`
|
|
)
|
|
.doesNotExist();
|
|
});
|
|
|
|
test("switching from events-type to a regular category clears the unedited skeleton", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
assert.dom(".d-editor-input").hasValue(/^\[event /);
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("discourse_post_event.composer.create_event_button"));
|
|
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(1);
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.hasNoValue("unedited event skeleton is cleared");
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"));
|
|
});
|
|
|
|
test("manually removing the event bbcode exits event mode", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("discourse_post_event.composer.create_event_button"));
|
|
|
|
await fillIn(".d-editor-input", "just a regular topic body");
|
|
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"));
|
|
assert.notStrictEqual(
|
|
document.querySelector("#reply-title").getAttribute("placeholder"),
|
|
i18n("discourse_post_event.composer.event_title_placeholder"),
|
|
"title placeholder is no longer the event one"
|
|
);
|
|
});
|
|
|
|
test("manually adding an event block back re-enters event mode", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
// Exit by deleting the auto-inserted skeleton
|
|
await fillIn(".d-editor-input", "");
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"));
|
|
|
|
// Now paste in an event block by hand
|
|
await fillIn(
|
|
".d-editor-input",
|
|
'[event start="2026-05-21 10:00" status="public" timezone="UTC"]\n[/event]'
|
|
);
|
|
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("discourse_post_event.composer.create_event_button"));
|
|
});
|
|
|
|
test("pasting an event block into a topic-mode reply enters event mode", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
await fillIn(".d-editor-input", "");
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"), "starts in topic mode");
|
|
|
|
await fillIn(
|
|
".d-editor-input",
|
|
'[event start="2026-06-15 10:00" status="public" timezone="UTC"]\n[/event]'
|
|
);
|
|
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(
|
|
i18n("discourse_post_event.composer.create_event_button"),
|
|
"auto-enters event mode"
|
|
);
|
|
});
|
|
|
|
test("switching to an events category with existing content does not auto-enter event mode", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(1);
|
|
await fillIn(".d-editor-input", "draft I started in a regular category");
|
|
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.hasValue(
|
|
"draft I started in a regular category",
|
|
"existing reply is left untouched"
|
|
);
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"), "stays in topic mode");
|
|
|
|
const composerActions = selectKit(".composer-actions");
|
|
await composerActions.expand();
|
|
assert
|
|
.dom(`.composer-actions .select-kit-row[data-value="create_event"]`)
|
|
.exists("Create event is still offered as an explicit opt-in");
|
|
});
|
|
|
|
test("an edited event block is preserved on category switch", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
const original = document.querySelector(".d-editor-input").value;
|
|
const edited = original.replace(`status="public"`, `status="private"`);
|
|
await fillIn(".d-editor-input", edited);
|
|
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(1);
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.hasValue(edited, "user-edited content survives the switch");
|
|
});
|
|
});
|
|
|
|
acceptance(
|
|
"Create event composer action — user without create-event permission",
|
|
function (needs) {
|
|
needs.user({ can_create_discourse_post_event: false });
|
|
needs.settings({
|
|
calendar_enabled: true,
|
|
discourse_post_event_enabled: true,
|
|
discourse_post_event_allowed_on_groups: "",
|
|
discourse_post_event_allowed_custom_fields: "",
|
|
});
|
|
|
|
needs.hooks.afterEach(function () {
|
|
_mutatedCategoryIds.forEach((id) => {
|
|
const category = Site.current().categories.find((c) => c.id === id);
|
|
if (category) {
|
|
category.categoryTypes = {};
|
|
}
|
|
});
|
|
_mutatedCategoryIds.clear();
|
|
});
|
|
|
|
test("events-type category stays in regular topic mode", async function (assert) {
|
|
markCategoryAsEvents(2);
|
|
|
|
await visit("/");
|
|
await click("#create-topic");
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.hasNoValue("event skeleton is not inserted");
|
|
assert
|
|
.dom(".save-or-cancel button.create")
|
|
.hasText(i18n("composer.create_topic"));
|
|
|
|
const composerActions = selectKit(".composer-actions");
|
|
await composerActions.expand();
|
|
assert
|
|
.dom(`.composer-actions .select-kit-row[data-value="create_event"]`)
|
|
.doesNotExist("Create event option is not offered");
|
|
assert
|
|
.dom(
|
|
`.composer-actions .select-kit-row[data-value="create_regular_topic"]`
|
|
)
|
|
.doesNotExist();
|
|
});
|
|
}
|
|
);
|