discourse/plugins/discourse-solved/test/javascripts/acceptance/discourse-solved-test.js
Régis Hanol bb235b5540
UX: Add high context topic card layout option to Horizon theme (#36902)
## Summary

The existing column-based topic list API works well for simple layouts,
but becomes unwieldy when trying to create richer card designs. Adding
features like assigned users, vote counts, solved status, tags, and
excerpts to the grid system required increasingly complex CSS hacks and
left awkward empty spaces when optional elements weren't present.

This adds a new "high context" layout mode that takes a different
approach: instead of fighting the column system, we use the
topic-list-columns API to replace all columns with a single component
that has full control over its internal layout. This gives us the
flexibility to arrange content in logical rows (header, content,
context, footer) using simple flexbox, without the rigidity of the grid.

## Changes

### Horizon Theme

The new `topic_card_context` theme setting lets admins choose between:
- **Simple** - the existing compact layout
- **High Context (experimental)** - rich cards with additional
information

The high context cards display:
- Creator avatar and username with timestamp
- Solved/unsolved status pill (when discourse-solved is enabled)
- Hot/pinned status indicators
- Topic title with status icons
- Post excerpt
- Last reply info with relative timestamp
- Assigned users including post-level assignments (when discourse-assign
is enabled)
- Vote count badge (when discourse-topic-voting is enabled)
- Category badge and tags
- Reply and like counts
- Unread indicator and post badges

The layout is only applied to public topic list routes (discovery and
tag routes), falling back to simple layout on private messages, user
activity, and bookmarks pages.

### Core Changes

**BEM naming for topic status classes:**
- All status classes now use `--modifier` pattern: `--bookmarked`,
`--closed`, `--archived`, `--warning`, `--personal-message`, `--pinned`,
`--unpinned`, `--invisible`
- Updated discourse-solved plugin to use `--solved` and `--unsolved`

**Code cleanup:**
- Extracted duplicated bulk selection checkbox into reusable
`BulkSelectCheckbox` component
- Removed unused `newDotText` getter from topic-cell
- Added `className` parameter to `render-tags` helper for flexibility

---------

Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
2026-01-14 14:15:03 +01:00

50 lines
1.7 KiB
JavaScript

import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { cloneJSON } from "discourse/lib/object";
import pretender, {
fixturesByUrl,
response,
} from "discourse/tests/helpers/create-pretender";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { postStreamWithAcceptedAnswerExcerpt } from "../helpers/discourse-solved-helpers";
acceptance(`Discourse Solved Plugin`, function (needs) {
needs.user();
test("A topic with an accepted answer shows an excerpt of the answer, if provided", async function (assert) {
pretender.get("/t/11.json", () =>
response(postStreamWithAcceptedAnswerExcerpt("this is an excerpt"))
);
pretender.get("/t/12.json", () =>
response(postStreamWithAcceptedAnswerExcerpt(null))
);
await visit("/t/with-excerpt/11");
assert.dom(".quote.accepted-answer blockquote").exists();
assert
.dom(".quote.accepted-answer blockquote")
.hasText("this is an excerpt");
await visit("/t/without-excerpt/12");
assert.dom(".quote.accepted-answer.title-only").exists();
assert.dom(".quote.accepted-answer blockquote").doesNotExist();
});
test("Full page search displays solved status", async function (assert) {
pretender.get("/search", () => {
const fixtures = cloneJSON(fixturesByUrl["/search.json"]);
fixtures.topics[0].has_accepted_answer = true;
return response(fixtures);
});
await visit("/search");
await fillIn(".search-query", "discourse");
await click(".search-cta");
assert.dom(".fps-topic").exists({ count: 1 }, "has one post");
assert.dom(".topic-statuses .--solved").exists("shows the right icon");
});
});