FEATURE: adds support for event preview in rich editor (#708)

Adds support to parsing/serializing/displaying an event preview representation for the rich editor view.

Co-authored-by: Renato Atilio <renato@discourse.org>
This commit is contained in:
Joffrey JAFFEUX 2025-03-28 21:14:02 +01:00 committed by GitHub
parent 7674040f4f
commit cbebb8da6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 141 additions and 4 deletions

View File

@ -1,3 +1,4 @@
< 3.5.0.beta2-dev: c1031224a552ea01958d153350c2d1254b323579
< 3.5.0.beta1-dev: fdf3ad927744a9dbb826cc46e489cca8ad469044
< 3.4.0.beta2-dev: 5f6942c1f3616d43eec18a71d76baa0e55e1340b
< 3.4.0.beta1-dev: 908ad614bc412f831f929ca726a4bda0b9ccaab6

View File

@ -1,10 +1,11 @@
import { isTesting } from "discourse/lib/environment";
import { withPluginApi } from "discourse/lib/plugin-api";
import I18n, { i18n } from "discourse-i18n";
import DiscoursePostEvent from "discourse/plugins/discourse-calendar/discourse/components/discourse-post-event";
import DiscoursePostEventEvent from "discourse/plugins/discourse-calendar/discourse/models/discourse-post-event-event";
import guessDateFormat from "../lib/guess-best-date-format";
function _validEventPreview(eventContainer) {
export function buildEventPreview(eventContainer) {
eventContainer.innerHTML = "";
eventContainer.classList.add("discourse-post-event-preview");
@ -34,13 +35,14 @@ function _validEventPreview(eventContainer) {
);
const format = guessDateFormat(startsAt, endsAt);
const guessedTz = isTesting() ? "UTC" : moment.tz.guess();
let datesString = `<span class='start'>${startsAt
.tz(moment.tz.guess())
.tz(guessedTz)
.format(format)}</span>`;
if (endsAt) {
datesString += ` → <span class='end'>${endsAt
.tz(moment.tz.guess())
.tz(guessedTz)
.format(format)}</span>`;
}
datesContainer.innerHTML = datesString;
@ -67,7 +69,7 @@ function _decorateEventPreview(api, cooked) {
if (index > 0) {
_invalidEventPreview(eventContainer);
} else {
_validEventPreview(eventContainer);
buildEventPreview(eventContainer);
}
});
}

View File

@ -0,0 +1,100 @@
import { camelize } from "@ember/string";
import { withPluginApi } from "discourse/lib/plugin-api";
import { buildEventPreview } from "../initializers/discourse-post-event-decorator";
const EVENT_ATTRIBUTES = {
name: { default: null },
start: { default: null },
end: { default: null },
reminders: { default: null },
minimal: { default: null },
closed: { default: null },
status: { default: "public" },
timezone: { default: "UTC" },
allowedGroups: { default: null },
};
/** @type {RichEditorExtension} */
const extension = {
nodeSpec: {
event: {
attrs: EVENT_ATTRIBUTES,
group: "block",
defining: true,
isolating: true,
draggable: true,
parseDOM: [
{
tag: "div.discourse-post-event",
getAttrs(dom) {
return { ...dom.dataset };
},
},
],
toDOM(node) {
const element = document.createElement("div");
element.classList.add("discourse-post-event");
for (const [key, value] of Object.entries(node.attrs)) {
if (value !== null) {
element.dataset[key] = value;
}
}
buildEventPreview(element);
return element;
},
},
},
parse: {
wrap_bbcode(state, token) {
if (token.tag === "div") {
if (token.nesting === -1 && state.top().type.name === "event") {
state.closeNode();
return true;
}
if (
token.nesting === 1 &&
token.attrGet("class") === "discourse-post-event"
) {
const attrs = Object.fromEntries(
token.attrs
.filter(([key]) => key.startsWith("data-"))
.map(([key, value]) => [camelize(key.slice(5)), value])
);
state.openNode(state.schema.nodes.event, attrs);
return true;
}
}
return false;
},
},
serializeNode: {
event(state, node) {
let bbcode = "[event";
Object.entries(node.attrs).forEach(([key, value]) => {
if (value !== null) {
bbcode += ` ${key}="${value}"`;
}
});
bbcode += "]\n[/event]\n";
state.write(bbcode);
},
},
};
export default {
initialize() {
withPluginApi("2.1.1", (api) => {
api.registerRichEditorExtension(extension);
});
},
};

View File

@ -0,0 +1,34 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
module("Integration | Component | rich-editor-extension", function (hooks) {
setupRenderingTest(hooks);
const testCases = {
"event alone": [
[
`[event start="2025-03-21 15:41" status="public" timezone="Europe/Paris" allowedGroups="trust_level_0"]\n[/event]\n`,
`<div class="discourse-post-event discourse-post-event-preview ProseMirror-selectednode" data-start="2025-03-21 15:41" data-status="public" data-timezone="Europe/Paris" data-allowed-groups="trust_level_0" contenteditable="false" draggable="true"><div class="event-preview-status">Public</div><div class="event-preview-dates"><span class="start">March 21, 2025 2:41 PM</span></div></div>`,
`[event start="2025-03-21 15:41" status="public" timezone="Europe/Paris" allowedGroups="trust_level_0"]\n[/event]\n`,
],
],
"event with content around": [
[
`Hello world\n\n[event start="2025-03-21 15:41" status="public" timezone="Europe/Paris" allowedGroups="trust_level_0"]\n[/event]\nGoodbye world`,
`<p>Hello world</p><div class="discourse-post-event discourse-post-event-preview" data-start="2025-03-21 15:41" data-status="public" data-timezone="Europe/Paris" data-allowed-groups="trust_level_0" contenteditable="false" draggable="true"><div class="event-preview-status">Public</div><div class="event-preview-dates"><span class="start">March 21, 2025 2:41 PM</span></div></div><p>Goodbye world</p>`,
`Hello world\n\n[event start="2025-03-21 15:41" status="public" timezone="Europe/Paris" allowedGroups="trust_level_0"]\n[/event]\nGoodbye world`,
],
],
};
Object.entries(testCases).forEach(([name, tests]) => {
tests.forEach(([markdown, expectedHtml, expectedMarkdown]) => {
test(name, async function (assert) {
this.siteSettings.rich_editor = true;
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
});
});
});
});