mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-05 08:41:11 +08:00
This change enables local dates to render properly in poll options and
improves how dates are displayed in the rich editor.
What changed:
- Rich editor now displays localized, human-friendly dates (e.g., "Today
at 2:30 PM") instead of raw date strings ("2021-01-01 12:00:00") by
using LocalDateBuilder
- Poll options can now include local dates that are properly decorated
and interactive
- Added keyboard shortcut (Cmd/Ctrl+Shift+.) to insert current date
markup in poll option inputs
- Extracted reusable utilities: formatLocalDate for rendering and
generateCurrentDateMarkup for BBCode generation
How it works:
- New NULL_HELPER export in decorated-html provides a null-safe helper
object for contexts without a post/model, allowing HTML decorators to
work in poll options
- New Ember modifiers (decorate-cooked-content, decorate-poll-option)
apply decorators to poll content
- Tests updated to verify data attributes rather than text content,
since formatted dates vary by timezone
Internal ref - t/155024
**BEFORE/AFTER (markdown editor)**
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 11 59 22"
src="https://github.com/user-attachments/assets/ff9fbb2b-0f1b-41b7-b0fc-b59c73ed4e32"
/>
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 11 59 42"
src="https://github.com/user-attachments/assets/6b07beab-e864-4319-b97b-fe7abe2180e6"
/>
**BEFORE/AFTER (rich text editor)**
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 12 00 05"
src="https://github.com/user-attachments/assets/0f0e1a4b-0829-4e0e-94ac-90f16c4e52d5"
/>
<img width="1699" height="1327" alt="CleanShot 2025-12-16 at 11 59 52"
src="https://github.com/user-attachments/assets/d8db2e7a-c110-41bd-9fbf-b4af0f5de2c5"
/>
232 lines
6.3 KiB
Text
232 lines
6.3 KiB
Text
import { click, render } from "@ember/test-helpers";
|
|
import { module, test } from "qunit";
|
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
import { i18n } from "discourse-i18n";
|
|
import PollOptions from "discourse/plugins/poll/discourse/components/poll-options";
|
|
|
|
const OPTIONS = [
|
|
{ id: "1ddc47be0d2315b9711ee8526ca9d83f", html: "This", votes: 0, rank: 0 },
|
|
{ id: "70e743697dac09483d7b824eaadb91e1", html: "That", votes: 0, rank: 0 },
|
|
{ id: "6c986ebcde3d5822a6e91a695c388094", html: "Other", votes: 0, rank: 0 },
|
|
];
|
|
|
|
const IMAGE_OPTIONS = [
|
|
{
|
|
id: "1ddc47be0d2315b9711ee8526ca9d83f",
|
|
html: "<img src='upload://tpbXHFLPCTLWjyGvtyekmXQN49A.jpeg'></img>",
|
|
votes: 0,
|
|
rank: 0,
|
|
},
|
|
{
|
|
id: "70e743697dac09483d7b824eaadb91e1",
|
|
html: "<img src='upload://eurierXHFETLWjHsdfLKKJDFLKJ.jpeg'></img>",
|
|
votes: 0,
|
|
rank: 0,
|
|
},
|
|
];
|
|
|
|
module("Poll | Component | poll-options", function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
|
|
test("single, not selected", async function (assert) {
|
|
this.setProperties({
|
|
isCheckbox: false,
|
|
isRankedChoice: false,
|
|
rankedChoiceDropdownContent: [],
|
|
options: OPTIONS,
|
|
votes: [],
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@isRankedChoice={{this.isRankedChoice}}
|
|
@ranked_choice_dropdown_content={{this.ranked_choice_dropdown_content}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendRadioClick={{this.toggleOption}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
assert.dom("li .d-icon-far-circle:nth-of-type(1)").exists({ count: 3 });
|
|
});
|
|
|
|
test("single, selected", async function (assert) {
|
|
this.setProperties({
|
|
isCheckbox: false,
|
|
isRankedChoice: false,
|
|
rankedChoiceDropdownContent: [],
|
|
options: OPTIONS,
|
|
votes: ["6c986ebcde3d5822a6e91a695c388094"],
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@isRankedChoice={{this.isRankedChoice}}
|
|
@ranked_choice_dropdown_content={{this.ranked_choice_dropdown_content}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendRadioClick={{this.toggleOption}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
assert.dom("li .d-icon-circle:nth-of-type(1)").exists({ count: 1 });
|
|
});
|
|
|
|
test("multi, not selected", async function (assert) {
|
|
this.setProperties({
|
|
isCheckbox: true,
|
|
isRankedChoice: false,
|
|
rankedChoiceDropdownContent: [],
|
|
options: OPTIONS,
|
|
votes: [],
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@isRankedChoice={{this.isRankedChoice}}
|
|
@ranked_choice_dropdown_content={{this.ranked_choice_dropdown_content}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendRadioClick={{this.toggleOption}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
assert.dom("li .d-icon-far-square:nth-of-type(1)").exists({ count: 3 });
|
|
});
|
|
|
|
test("multi, selected", async function (assert) {
|
|
this.setProperties({
|
|
isCheckbox: true,
|
|
isRankedChoice: false,
|
|
rankedChoiceDropdownContent: [],
|
|
options: OPTIONS,
|
|
votes: ["6c986ebcde3d5822a6e91a695c388094"],
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@isRankedChoice={{this.isRankedChoice}}
|
|
@ranked_choice_dropdown_content={{this.ranked_choice_dropdown_content}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendRadioClick={{this.toggleOption}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
assert
|
|
.dom("li .d-icon-far-square-check:nth-of-type(1)")
|
|
.exists({ count: 1 });
|
|
});
|
|
|
|
test("single with images", async function (assert) {
|
|
this.setProperties({
|
|
isCheckbox: false,
|
|
options: IMAGE_OPTIONS,
|
|
votes: [],
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendRadioClick={{this.toggleOption}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
assert.dom("li img").exists({ count: 2 });
|
|
});
|
|
|
|
test("ranked choice - priorities", async function (assert) {
|
|
this.setProperties({
|
|
isCheckbox: false,
|
|
isRankedChoice: true,
|
|
rankedChoiceDropdownContent: [],
|
|
options: OPTIONS,
|
|
votes: [],
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@isRankedChoice={{this.isRankedChoice}}
|
|
@ranked_choice_dropdown_content={{this.ranked_choice_dropdown_content}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendRadioClick={{this.toggleOption}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
await click(
|
|
`.ranked-choice-poll-option[data-poll-option-id='${OPTIONS[0].id}'] button`
|
|
);
|
|
|
|
assert
|
|
.dom(".dropdown-menu__item:nth-child(2)")
|
|
.hasText(`1 ${i18n("poll.options.ranked_choice.highest_priority")}`);
|
|
|
|
assert
|
|
.dom(".dropdown-menu__item:nth-child(4)")
|
|
.hasText(`3 ${i18n("poll.options.ranked_choice.lowest_priority")}`);
|
|
});
|
|
|
|
test("clicking on local date does not select option", async function (assert) {
|
|
const DATE_OPTIONS = [
|
|
{
|
|
id: "1ddc47be0d2315b9711ee8526ca9d83f",
|
|
html: 'Meeting on <span class="discourse-local-date" data-date="2025-01-15">Jan 15</span>',
|
|
votes: 0,
|
|
rank: 0,
|
|
},
|
|
];
|
|
|
|
let optionSelected = false;
|
|
this.setProperties({
|
|
isCheckbox: false,
|
|
isRankedChoice: false,
|
|
options: DATE_OPTIONS,
|
|
votes: [],
|
|
sendOptionSelect: () => (optionSelected = true),
|
|
});
|
|
|
|
await render(
|
|
<template>
|
|
<PollOptions
|
|
@isCheckbox={{this.isCheckbox}}
|
|
@isRankedChoice={{this.isRankedChoice}}
|
|
@options={{this.options}}
|
|
@votes={{this.votes}}
|
|
@sendOptionSelect={{this.sendOptionSelect}}
|
|
/>
|
|
</template>
|
|
);
|
|
|
|
await click(".discourse-local-date");
|
|
assert.false(
|
|
optionSelected,
|
|
"option should not be selected when clicking on date"
|
|
);
|
|
|
|
await click("li button");
|
|
assert.true(
|
|
optionSelected,
|
|
"option should be selected when clicking on button"
|
|
);
|
|
});
|
|
});
|