Skip to content

Commit 22c8b6e

Browse files
committed
Add filter button in reaction summary details
1 parent f5ff72d commit 22c8b6e

12 files changed

Lines changed: 269 additions & 49 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<button
2+
type="button"
3+
class="button small{if !$reactionTypeID} active{/if}"
4+
data-filter-reaction-type-id="0"
5+
data-list-view-id="{$view->getID()}"
6+
>
7+
<span>{lang}wcf.like.reaction.all{/lang}</span>
8+
<span class="badge">{#$totalCount}</span>
9+
</button>
10+
11+
{foreach from=$reactionTypes item='reactionType'}
12+
<button
13+
type="button"
14+
class="button small{if $reactionType->reactionTypeID === $reactionTypeID} active{/if}"
15+
data-filter-reaction-type-id="{$reactionType->reactionTypeID}"
16+
data-list-view-id="{$view->getID()}"
17+
>
18+
{unsafe:$reactionType->renderIcon()}
19+
<span>{$reactionType->getTitle()}</span>
20+
<span class="badge">{#$reactionCounts[$reactionType->reactionTypeID]}</span>
21+
</button>
22+
{/foreach}
23+
24+
<script data-relocate="true">
25+
require(['WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons'], ({ setup }) => {
26+
setup('{unsafe:$view->getID()|encodeJS}');
27+
});
28+
</script>

com.woltlab.wcf/templates/shared_listView.tpl

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
<div class="listView">
2-
{if $view->isSortable() || $view->isFilterable() || $view->hasBulkInteractions()}
2+
{if $view->isSortable() || $view->isFilterable() || $view->hasBulkInteractions() || $view->getAdditionalHeaderContent()}
33
<div class="listView__header">
4+
{if $view->getAdditionalHeaderContent()}
5+
<div class="listView__header__additionalContent">
6+
{unsafe:$view->getAdditionalHeaderContent()}
7+
</div>
8+
{/if}
9+
410
{if $view->isFilterable()}
511
<div class="listView__filters" id="{$view->getID()}_filters">
612
{foreach from=$view->getActiveFilters() item='value' key='key'}
@@ -17,53 +23,57 @@
1723
{/foreach}
1824
</div>
1925
{/if}
20-
<div class="listView__header__buttons">
21-
{if $view->hasAvailableInteractions()}
22-
<div class="listView__header__button">
23-
<button type="button" class="button small listView__editMode__toggle">
24-
{icon name='pencil'}
25-
<span>{lang}wcf.global.button.edit{/lang}</span>
26-
</button>
26+
{hascontent}
27+
<div class="listView__header__buttons">
28+
{content}
29+
{if $view->hasAvailableInteractions()}
30+
<div class="listView__header__button">
31+
<button type="button" class="button small listView__editMode__toggle">
32+
{icon name='pencil'}
33+
<span>{lang}wcf.global.button.edit{/lang}</span>
34+
</button>
2735

28-
{if $view->hasBulkInteractions()}
29-
<label class="listView__selectAllItems__label jsTooltip" title="{lang}wcf.clipboard.item.markAll{/lang}">
30-
<input type="checkbox" id="{$view->getID()}_selectAllItems" class="listView__selectAllItems"
31-
aria-label="{lang}wcf.clipboard.item.markAll{/lang}">
32-
</label>
36+
{if $view->hasBulkInteractions()}
37+
<label class="listView__selectAllItems__label jsTooltip" title="{lang}wcf.clipboard.item.markAll{/lang}">
38+
<input type="checkbox" id="{$view->getID()}_selectAllItems" class="listView__selectAllItems"
39+
aria-label="{lang}wcf.clipboard.item.markAll{/lang}">
40+
</label>
41+
{/if}
42+
</div>
3343
{/if}
34-
</div>
35-
{/if}
36-
{if $view->isSortable()}
37-
<div class="listView__header__button dropdown">
38-
<button type="button" class="button small dropdownToggle">
39-
{icon name='arrow-down-short-wide'}
40-
<span>{lang}wcf.global.sorting{/lang}</span>
41-
</button>
42-
<ul class="dropdownMenu" id="{$view->getID()}_sorting">
43-
{foreach from=$view->getAvailableSortFields() item='sortField'}
44-
<li>
45-
<button type="button" class="listView__sorting__button" data-sort-id="{$sortField->id}">
46-
{unsafe:$sortField}
47-
</button>
48-
</li>
49-
{/foreach}
50-
</ul>
51-
</div>
52-
{/if}
53-
{if $view->isFilterable()}
54-
<div class="listView__header__button">
55-
<button type="button" class="button small" id="{$view->getID()}_filterButton" data-endpoint="{$view->getFilterActionEndpoint()}">
56-
{icon name='sliders'}
57-
{lang}wcf.global.filter{/lang}
58-
</button>
59-
</div>
60-
{/if}
61-
{if $view->getPrimaryButton()}
62-
<div class="listView__header__button">
63-
{unsafe:$view->getPrimaryButton()->render()}
64-
</div>
65-
{/if}
66-
</div>
44+
{if $view->isSortable()}
45+
<div class="listView__header__button dropdown">
46+
<button type="button" class="button small dropdownToggle">
47+
{icon name='arrow-down-short-wide'}
48+
<span>{lang}wcf.global.sorting{/lang}</span>
49+
</button>
50+
<ul class="dropdownMenu" id="{$view->getID()}_sorting">
51+
{foreach from=$view->getAvailableSortFields() item='sortField'}
52+
<li>
53+
<button type="button" class="listView__sorting__button" data-sort-id="{$sortField->id}">
54+
{unsafe:$sortField}
55+
</button>
56+
</li>
57+
{/foreach}
58+
</ul>
59+
</div>
60+
{/if}
61+
{if $view->isFilterable()}
62+
<div class="listView__header__button">
63+
<button type="button" class="button small" id="{$view->getID()}_filterButton" data-endpoint="{$view->getFilterActionEndpoint()}">
64+
{icon name='sliders'}
65+
{lang}wcf.global.filter{/lang}
66+
</button>
67+
</div>
68+
{/if}
69+
{if $view->getPrimaryButton()}
70+
<div class="listView__header__button">
71+
{unsafe:$view->getPrimaryButton()->render()}
72+
</div>
73+
{/if}
74+
{/content}
75+
</div>
76+
{/hascontent}
6777
</div>
6878
{/if}
6979

ts/WoltLabSuite/Core/Component/ListView.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ export class ListView {
128128
this.#viewElement.addEventListener("interaction:reset-selection", () => {
129129
this.#state.resetSelection();
130130
});
131+
132+
this.#viewElement.addEventListener("interaction:set-parameters", (event: CustomEvent) => {
133+
for (const key of Object.keys(event.detail)) {
134+
if (this.#listViewParameters === undefined) {
135+
this.#listViewParameters = new Map();
136+
}
137+
138+
if (event.detail[key] === undefined) {
139+
this.#listViewParameters.delete(key);
140+
} else {
141+
this.#listViewParameters.set(key, event.detail[key]);
142+
}
143+
}
144+
145+
void this.#loadItems(StateChangeCause.Change);
146+
});
131147
}
132148

133149
#setupState(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Handles the filter buttons in the reaction summary details dialog.
3+
*
4+
* @author Marcel Werk
5+
* @copyright 2001-2026 WoltLab GmbH
6+
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7+
* @since 6.3
8+
*/
9+
10+
export function setup(listViewId: string): void {
11+
document
12+
.querySelectorAll<HTMLButtonElement>(`[data-filter-reaction-type-id][data-list-view-id="${listViewId}"]`)
13+
.forEach((button) => {
14+
button.addEventListener("click", () => {
15+
if (button.classList.contains("active")) {
16+
return;
17+
}
18+
19+
document
20+
.querySelectorAll<HTMLButtonElement>(`[data-filter-reaction-type-id][data-list-view-id="${listViewId}"]`)
21+
.forEach((button) => {
22+
button.classList.remove("active");
23+
});
24+
25+
button.classList.add("active");
26+
27+
let reactionTypeID: string | undefined = undefined;
28+
if (button.dataset.filterReactionTypeId && button.dataset.filterReactionTypeId !== "0") {
29+
reactionTypeID = button.dataset.filterReactionTypeId;
30+
}
31+
const listView = document.getElementById(`${listViewId}_items`);
32+
listView!.dispatchEvent(new CustomEvent("interaction:set-parameters", { detail: { reactionTypeID } }));
33+
});
34+
});
35+
}

wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.js

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void
162162

163163
$event->preload('wcf.reactions.react');
164164
$event->preload('wcf.reactions.summary.listReactions');
165+
$event->preload('wcf.reactions.summary.title');
165166

166167
$event->preload('wcf.style.changeStyle');
167168

wcfsetup/install/files/lib/system/listView/AbstractListView.class.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ abstract class AbstractListView
5454
private int $fixedNumberOfItems = 0;
5555
private string $markAsReadEndpoint = '';
5656
private ?ListViewPrimaryButton $primaryButton = null;
57+
private string $additionalHeaderContent = '';
5758

5859
/**
5960
* @var array<string, string>
@@ -816,6 +817,22 @@ private function init(): void
816817
}
817818
}
818819

820+
/**
821+
* @since 6.3
822+
*/
823+
public function setAdditionalHeaderContent(string $content): void
824+
{
825+
$this->additionalHeaderContent = $content;
826+
}
827+
828+
/**
829+
* @since 6.3
830+
*/
831+
public function getAdditionalHeaderContent(): string
832+
{
833+
return $this->additionalHeaderContent;
834+
}
835+
819836
/**
820837
* @return TDatabaseObjectList
821838
*/

wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class ReactionSummaryDetailsListView extends AbstractListView
2929
{
3030
public function __construct(
3131
public readonly string $objectType,
32-
public readonly int $objectID
32+
public readonly int $objectID,
33+
public readonly ?int $reactionTypeID = null,
3334
) {
3435
$this->addAvailableSortFields([
3536
new ListViewSortField(
@@ -45,6 +46,7 @@ public function __construct(
4546
$this->setItemsPerPage(100);
4647
$this->setCssClassName('simpleUserList');
4748
$this->setContainerCssClassName('simpleUserList__container');
49+
$this->setAdditionalHeaderContent($this->getSimpleFilterButtons());
4850
}
4951

5052
#[\Override]
@@ -55,6 +57,9 @@ protected function createObjectList(): ViewableLikeList
5557
ReactionHandler::getInstance()->getObjectType($this->objectType)->objectTypeID
5658
]);
5759
$likeList->getConditionBuilder()->add('objectID = ?', [$this->objectID]);
60+
if ($this->reactionTypeID !== null) {
61+
$likeList->getConditionBuilder()->add('reactionTypeID = ?', [$this->reactionTypeID]);
62+
}
5863

5964
return $likeList;
6065
}
@@ -102,8 +107,6 @@ public function renderInteractionContextMenuButton(DatabaseObject $item): string
102107
return '';
103108
}
104109

105-
\assert($item instanceof ViewableLike);
106-
107110
return $this->getInteractionContextMenuComponent()->renderButton($item->getUserProfile());
108111
}
109112

@@ -112,4 +115,56 @@ protected function getInitializedEvent(): ReactionSummaryDetailsListViewInitiali
112115
{
113116
return new ReactionSummaryDetailsListViewInitialized($this);
114117
}
118+
119+
#[\Override]
120+
public function getParameters(): array
121+
{
122+
$parameters = [
123+
'objectType' => $this->objectType,
124+
'objectID' => $this->objectID,
125+
];
126+
127+
if ($this->reactionTypeID !== null) {
128+
$parameters['reactionTypeID'] = $this->reactionTypeID;
129+
}
130+
131+
return $parameters;
132+
}
133+
134+
private function getSimpleFilterButtons(): string
135+
{
136+
$objectType = ReactionHandler::getInstance()->getObjectType($this->objectType);
137+
if ($objectType === null) {
138+
return '';
139+
}
140+
141+
$sql = "SELECT COUNT(*) AS count, reactionTypeID FROM wcf1_like WHERE objectTypeID = ? AND objectID = ? GROUP BY reactionTypeID";
142+
$statement = WCF::getDB()->prepare($sql);
143+
$statement->execute([$objectType->objectTypeID, $this->objectID]);
144+
$reactionCounts = $statement->fetchMap('reactionTypeID', 'count');
145+
if (\count($reactionCounts) <= 1) {
146+
// Skip filtering if only one type is present.
147+
return '';
148+
}
149+
150+
$totalCount = 0;
151+
foreach ($reactionCounts as $count) {
152+
$totalCount += $count;
153+
}
154+
155+
return WCF::getTPL()->render(
156+
'wcf',
157+
'reactionSummaryDetailsFilterButtons',
158+
[
159+
'view' => $this,
160+
'totalCount' => $totalCount,
161+
'reactionCounts' => $reactionCounts,
162+
'reactionTypes' => \array_filter(
163+
ReactionHandler::getInstance()->getReactionTypes(),
164+
static fn($reactionType) => isset($reactionCounts[$reactionType->reactionTypeID])
165+
),
166+
'reactionTypeID' => $this->reactionTypeID,
167+
]
168+
);
169+
}
115170
}

0 commit comments

Comments
 (0)