diff --git a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl
new file mode 100644
index 0000000000..4e204d2fb5
--- /dev/null
+++ b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts
new file mode 100644
index 0000000000..f0eecbafa1
--- /dev/null
+++ b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts
@@ -0,0 +1,140 @@
+/**
+ * Provides a filter input for a categorized item list.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @sice 6.3
+ */
+
+import { innerError } from "WoltLabSuite/Core/Dom/Util";
+import { getPhrase } from "WoltLabSuite/Core/Language";
+import { escapeRegExp } from "WoltLabSuite/Core/StringUtil";
+
+type Item = {
+ element: HTMLLIElement;
+ span: HTMLSpanElement;
+ text: string;
+};
+
+type Category = {
+ items: Item[];
+ element: HTMLLIElement;
+};
+
+export class CategorizedItemList {
+ readonly #container: HTMLElement;
+ readonly #elementList: HTMLUListElement;
+ readonly #input: HTMLInputElement;
+ #value: string = "";
+ readonly #clearButton: HTMLButtonElement;
+ #categories: Category[] = [];
+ readonly #fragment: DocumentFragment;
+
+ constructor(elementId: string) {
+ this.#fragment = document.createDocumentFragment();
+
+ const container = document.getElementById(elementId);
+ if (!container) {
+ throw new Error(`Element with ID ${elementId} not found.`);
+ }
+
+ this.#container = container;
+ this.#elementList = this.#container.querySelector(".scrollableCheckboxList")!;
+
+ this.#input = this.#container.querySelector(".inputAddon > input") as HTMLInputElement;
+ this.#input.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ }
+ });
+ this.#input.addEventListener("keyup", () => this.#keyup());
+
+ this.#clearButton = this.#container.querySelector(".inputAddon > .clearButton")!;
+ this.#clearButton.addEventListener("click", (event) => {
+ event.preventDefault();
+
+ this.#input.value = "";
+ this.#keyup();
+ });
+
+ this.#buildItemMap();
+ }
+
+ #buildItemMap(): void {
+ let category: Category | null = null;
+ for (const li of this.#elementList.querySelectorAll(":scope > li")) {
+ const input = li.querySelector('input[type="radio"]');
+ if (input) {
+ if (!category) {
+ throw new Error("Input found without a preceding category.");
+ }
+
+ category.items.push({
+ element: li,
+ span: li.querySelector("span")!,
+ text: li.textContent!.trim(),
+ });
+ } else {
+ const items: Item[] = [];
+ category = {
+ items: items,
+ element: li,
+ };
+ this.#categories.push(category);
+ }
+ }
+ }
+
+ #keyup(): void {
+ const value = this.#input.value.trim();
+ if (this.#value === value) {
+ return;
+ }
+
+ this.#value = value;
+
+ if (this.#value) {
+ this.#clearButton.classList.remove("disabled");
+ } else {
+ this.#clearButton.classList.add("disabled");
+ }
+
+ // move list into fragment before editing items, increases performance
+ // by avoiding the browser to perform repaint/layout over and over again
+ this.#fragment.appendChild(this.#elementList);
+
+ this.#categories.forEach((category) => {
+ this.#filterItems(category);
+ });
+
+ const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null;
+
+ this.#container.insertAdjacentElement("beforeend", this.#elementList);
+
+ innerError(this.#container, hasVisibleItem ? false : getPhrase("wcf.global.filter.error.noMatches"));
+ }
+
+ #filterItems(category: Category): void {
+ const regexp = new RegExp("(" + escapeRegExp(this.#value) + ")", "i");
+
+ let hasMatchingItem = false;
+ for (const item of category.items) {
+ if (this.#value === "") {
+ item.span.innerHTML = item.text; // Reset highlighting
+
+ hasMatchingItem = true;
+ item.element.hidden = false;
+ } else if (regexp.test(item.text)) {
+ item.span.innerHTML = item.text.replace(regexp, "$1");
+
+ item.element.hidden = false;
+ hasMatchingItem = true;
+ } else {
+ item.element.hidden = true;
+ }
+ }
+
+ category.element.hidden = !hasMatchingItem;
+ }
+}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js
new file mode 100644
index 0000000000..92bd82ae7d
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js
@@ -0,0 +1,112 @@
+/**
+ * Provides a filter input for a categorized item list.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @sice 6.3
+ */
+define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil"], function (require, exports, Util_1, Language_1, StringUtil_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.CategorizedItemList = void 0;
+ class CategorizedItemList {
+ #container;
+ #elementList;
+ #input;
+ #value = "";
+ #clearButton;
+ #categories = [];
+ #fragment;
+ constructor(elementId) {
+ this.#fragment = document.createDocumentFragment();
+ const container = document.getElementById(elementId);
+ if (!container) {
+ throw new Error(`Element with ID ${elementId} not found.`);
+ }
+ this.#container = container;
+ this.#elementList = this.#container.querySelector(".scrollableCheckboxList");
+ this.#input = this.#container.querySelector(".inputAddon > input");
+ this.#input.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ }
+ });
+ this.#input.addEventListener("keyup", () => this.#keyup());
+ this.#clearButton = this.#container.querySelector(".inputAddon > .clearButton");
+ this.#clearButton.addEventListener("click", (event) => {
+ event.preventDefault();
+ this.#input.value = "";
+ this.#keyup();
+ });
+ this.#buildItemMap();
+ }
+ #buildItemMap() {
+ let category = null;
+ for (const li of this.#elementList.querySelectorAll(":scope > li")) {
+ const input = li.querySelector('input[type="radio"]');
+ if (input) {
+ if (!category) {
+ throw new Error("Input found without a preceding category.");
+ }
+ category.items.push({
+ element: li,
+ span: li.querySelector("span"),
+ text: li.textContent.trim(),
+ });
+ }
+ else {
+ const items = [];
+ category = {
+ items: items,
+ element: li,
+ };
+ this.#categories.push(category);
+ }
+ }
+ }
+ #keyup() {
+ const value = this.#input.value.trim();
+ if (this.#value === value) {
+ return;
+ }
+ this.#value = value;
+ if (this.#value) {
+ this.#clearButton.classList.remove("disabled");
+ }
+ else {
+ this.#clearButton.classList.add("disabled");
+ }
+ // move list into fragment before editing items, increases performance
+ // by avoiding the browser to perform repaint/layout over and over again
+ this.#fragment.appendChild(this.#elementList);
+ this.#categories.forEach((category) => {
+ this.#filterItems(category);
+ });
+ const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null;
+ this.#container.insertAdjacentElement("beforeend", this.#elementList);
+ (0, Util_1.innerError)(this.#container, hasVisibleItem ? false : (0, Language_1.getPhrase)("wcf.global.filter.error.noMatches"));
+ }
+ #filterItems(category) {
+ const regexp = new RegExp("(" + (0, StringUtil_1.escapeRegExp)(this.#value) + ")", "i");
+ let hasMatchingItem = false;
+ for (const item of category.items) {
+ if (this.#value === "") {
+ item.span.innerHTML = item.text; // Reset highlighting
+ hasMatchingItem = true;
+ item.element.hidden = false;
+ }
+ else if (regexp.test(item.text)) {
+ item.span.innerHTML = item.text.replace(regexp, "$1");
+ item.element.hidden = false;
+ hasMatchingItem = true;
+ }
+ else {
+ item.element.hidden = true;
+ }
+ }
+ category.element.hidden = !hasMatchingItem;
+ }
+ }
+ exports.CategorizedItemList = CategorizedItemList;
+});
diff --git a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php
index 83c4c2772b..193571b434 100644
--- a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php
+++ b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php
@@ -82,22 +82,14 @@ private function getForm(AbstractConditionProvider $provider): Psr15DialogForm
self::class,
WCF::getLanguage()->get('wcf.condition.add')
);
- $options = \array_map(
- static fn (IConditionType $conditionType) => WCF::getLanguage()->get($conditionType->getLabel()),
- $provider->getConditionTypes()
- );
- $collator = new \Collator(WCF::getLanguage()->getLocale());
- \uasort(
- $options,
- static fn (string $a, string $b) => $collator->compare($a, $b)
- );
$form->appendChild(
- SingleSelectionFormField::create('conditionType')
+ $this->getConditionTypeFormField()
+ ->id('conditionType')
->label('wcf.condition.condition')
->filterable()
->required()
- ->options($options)
+ ->options($this->getOptions($provider), true, false)
);
$form->markRequiredFields(false);
@@ -105,4 +97,71 @@ private function getForm(AbstractConditionProvider $provider): Psr15DialogForm
return $form;
}
+
+ /**
+ * @param AbstractConditionProvider> $provider
+ *
+ * @return array{}
+ */
+ private function getOptions(AbstractConditionProvider $provider): array
+ {
+ $conditionTypes = $provider->getConditionTypes();
+
+ $grouped = [];
+ foreach ($conditionTypes as $key => $conditionType) {
+ $category = $conditionType->getCategory();
+ $label = $conditionType->getLabel();
+
+ if (!isset($grouped[$category])) {
+ $grouped[$category] = [
+ 'items' => [],
+ 'label' => WCF::getLanguage()->get('wcf.condition.category.' . $category),
+ ];
+ }
+
+ $grouped[$category]['items'][$key] = WCF::getLanguage()->get($label);
+ }
+
+ $collator = new \Collator(WCF::getLanguage()->getLocale());
+
+ foreach ($grouped as &$category) {
+ \uasort($category['items'], static function ($labelA, $labelB) use ($collator) {
+ return $collator->compare($labelA, $labelB);
+ });
+ }
+ unset($category);
+
+ \uasort($grouped, static function ($catA, $catB) use ($collator) {
+ return $collator->compare($catA['label'], $catB['label']);
+ });
+
+ $options = [];
+
+ foreach ($grouped as $categoryKey => $category) {
+ $options[] = [
+ 'depth' => 0,
+ 'isSelectable' => false,
+ 'label' => $category['label'],
+ 'value' => $categoryKey,
+ ];
+
+ foreach ($category['items'] as $key => $label) {
+ $options[] = [
+ 'depth' => 1,
+ 'isSelectable' => true,
+ 'label' => $label,
+ 'value' => $key,
+ ];
+ }
+ }
+
+ return $options;
+ }
+
+ private function getConditionTypeFormField(): SingleSelectionFormField
+ {
+ return new class extends SingleSelectionFormField {
+ protected $templateName = 'shared_categorizedSingleSelectionFormField';
+ };
+ }
}
diff --git a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php
index 6ac0a8792b..b1d3736336 100644
--- a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php
+++ b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php
@@ -39,6 +39,7 @@ public function __construct()
new StringUserConditionType(
identifier: "username",
columnName: "username",
+ category: "user",
migrateKeyName: "username",
migrateConditionObjectType: 'com.woltlab.wcf.user.username'
),
@@ -47,6 +48,7 @@ public function __construct()
new StringUserConditionType(
identifier: "email",
columnName: "email",
+ category: "user",
migrateKeyName: "email",
migrateConditionObjectType: 'com.woltlab.wcf.user.email'
),
@@ -70,6 +72,7 @@ public function __construct()
new IsNullUserConditionType(
identifier: "avatar",
columnName: 'avatarFileID',
+ category: 'userProfile',
migrateKeyName: 'userAvatar',
migrateConditionObjectType: 'com.woltlab.wcf.user.avatar'
),
@@ -81,6 +84,7 @@ public function __construct()
new IsNullUserConditionType(
identifier: "coverPhoto",
columnName: 'coverPhotoFileID',
+ category: 'userProfile',
migrateKeyName: 'userCoverPhoto',
migrateConditionObjectType: 'com.woltlab.wcf.coverPhoto'
),
@@ -89,6 +93,7 @@ public function __construct()
new BooleanUserConditionType(
identifier: "isBanned",
columnName: 'banned',
+ category: 'user',
migrateKeyName: 'userIsBanned',
migrateConditionObjectType: 'com.woltlab.wcf.user.state'
),
@@ -100,6 +105,7 @@ public function __construct()
new IsNullUserConditionType(
identifier: "isEmailConfirmed",
columnName: 'emailConfirmed',
+ category: 'user',
migrateKeyName: 'userIsEmailConfirmed',
migrateConditionObjectType: 'com.woltlab.wcf.user.state'
),
@@ -108,6 +114,7 @@ public function __construct()
new BooleanUserConditionType(
identifier: "isMultifactorActive",
columnName: 'multifactorActive',
+ category: 'user',
migrateKeyName: 'multifactorActive',
migrateConditionObjectType: 'com.woltlab.wcf.user.multifactor'
),
@@ -122,6 +129,7 @@ public function __construct()
new IntegerUserConditionType(
identifier: "activityPoints",
columnName: "activityPoints",
+ category: 'userProfile',
migrateConditionObjectType: 'com.woltlab.wcf.user.activityPoints'
),
);
@@ -129,6 +137,7 @@ public function __construct()
new IntegerUserConditionType(
identifier: "likesReceived",
columnName: "likesReceived",
+ category: 'userProfile',
migrateConditionObjectType: 'com.woltlab.wcf.user.likesReceived'
),
);
@@ -136,6 +145,7 @@ public function __construct()
new IntegerUserConditionType(
identifier: "trophyPoints",
columnName: "trophyPoints",
+ category: 'userProfile',
migrateConditionObjectType: 'com.woltlab.wcf.user.trophyPoints'
),
);
diff --git a/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php
index 03b9aa4219..fee6e9f8c0 100644
--- a/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php
@@ -36,4 +36,11 @@ public function getLabel(): string;
* @param TFilter $filter
*/
public function setFilter(mixed $filter): void;
+
+ /**
+ * Get the name of the category for this condition type.
+ * All condition types with the same category are grouped together.
+ * The language variable for the category name is `wcf.condition.category.`.
+ */
+ public function getCategory(): string;
}
diff --git a/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php
index af5436701c..b1e075568a 100644
--- a/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php
@@ -47,6 +47,12 @@ public function matches(): bool
return \in_array(RequestHandler::getInstance()->getActivePageID(), $this->filter);
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "page";
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php
index fff8550465..ef6b2a5f2b 100644
--- a/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php
@@ -53,6 +53,12 @@ public function matches(): bool
return $dateTime->format('w') === $this->filter;
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "pointInTime";
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php
index f6853a99a6..c415c74f51 100644
--- a/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php
@@ -53,6 +53,12 @@ public function matches(): bool
return $dateTime->format('w') !== $this->filter;
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "pointInTime";
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php
index 3b80a49c73..1b29d932a1 100644
--- a/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php
@@ -49,6 +49,12 @@ public function matches(): bool
return RequestHandler::getInstance()->getActivePageID() !== (int)$this->filter;
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "page";
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php
index 2784c364ea..06aa7371f3 100644
--- a/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php
@@ -74,6 +74,12 @@ public function matches(): bool
};
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "pointInTime";
+ }
+
/**
* @return array
*/
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php
index c2ee9504da..561a9de226 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php
@@ -26,6 +26,7 @@ class BooleanUserConditionType extends AbstractConditionType implements IDatabas
public function __construct(
public readonly string $identifier,
public readonly string $columnName,
+ public readonly string $category,
public readonly ?string $migrateKeyName = null,
public readonly ?string $migrateConditionObjectType = null,
) {
@@ -69,6 +70,12 @@ public function matches(object $object): bool
}
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return $this->category;
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php
index 2c2306ae1b..523f232045 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php
@@ -71,6 +71,12 @@ public function matches(object $object): bool
return !\in_array((int)$this->filter, $trophyIDs, true);
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "userProfile";
+ }
+
/**
* @return Trophy[]
*/
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php
index 72b710fb7d..8333af3317 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php
@@ -71,6 +71,12 @@ public function matches(object $object): bool
return \in_array((int)$this->filter, $trophyIDs, true);
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "userProfile";
+ }
+
/**
* @return Trophy[]
*/
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php
index 3b57a26675..a64510d45b 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php
@@ -71,6 +71,12 @@ public function matches(object $object): bool
return \in_array((int)$this->filter, $object->getGroupIDs(), true);
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "user";
+ }
+
#[\Override]
public function canMigrateConditionData(string $objectType): bool
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php
index 5b427ff3a4..9319ece74e 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php
@@ -29,6 +29,7 @@ class IntegerUserConditionType extends AbstractConditionType implements IDatabas
public function __construct(
public readonly string $identifier,
public readonly string $columnName,
+ public readonly string $category,
public readonly ?string $migrateConditionObjectType = null,
) {
}
@@ -83,6 +84,12 @@ public function matches(object $object): bool
};
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return $this->category;
+ }
+
/**
* @return string[]
*/
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php
index 1a81c66eb1..84cdcd27b2 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php
@@ -14,7 +14,7 @@ final class IsEnabledConditionType extends BooleanUserConditionType
{
public function __construct()
{
- parent::__construct("isEnabled", 'activationCode', 'userIsEnabled', 'com.woltlab.wcf.user.state');
+ parent::__construct("isEnabled", 'activationCode', 'user', 'userIsEnabled', 'com.woltlab.wcf.user.state');
}
#[\Override]
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php
index 6f243bc89f..f032230ec9 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php
@@ -26,6 +26,7 @@ class IsNullUserConditionType extends AbstractConditionType implements IDatabase
public function __construct(
public readonly string $identifier,
public readonly string $columnName,
+ public readonly string $category,
public readonly ?string $migrateKeyName = null,
public readonly ?string $migrateConditionObjectType = null,
) {}
@@ -68,6 +69,12 @@ public function matches(object $object): bool
}
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return $this->category;
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php
index ba74a10afd..04ae4fa1d3 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php
@@ -61,6 +61,12 @@ public function matches(object $object): bool
return (int)$this->filter === $object->languageID;
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "user";
+ }
+
#[\Override]
public function migrateConditionData(array &$conditionData): array
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php
index 12731c3864..126edf1b97 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php
@@ -71,6 +71,12 @@ public function matches(object $object): bool
return !\in_array((int)$this->filter, $object->getGroupIDs(), true);
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "user";
+ }
+
#[\Override]
public function canMigrateConditionData(string $objectType): bool
{
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php
index 717d5b0fcc..51ecca460a 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php
@@ -79,6 +79,12 @@ public function matches(object $object): bool
};
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "user";
+ }
+
/**
* @return string[]
*/
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php
index febacbd506..dd26a01eec 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php
@@ -56,6 +56,12 @@ public function matches(object $object): bool
};
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return "user";
+ }
+
/**
* @return array{condition: string, timestamp: int}
*/
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php
index 30e5e74b8a..f8b8fffeac 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php
@@ -17,6 +17,7 @@ public function __construct()
parent::__construct(
'signature',
'signature',
+ 'userProfile',
'userSignature',
'com.woltlab.wcf.user.signature'
);
diff --git a/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php
index d3f6d022b1..6032f4eb5b 100644
--- a/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php
+++ b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php
@@ -30,6 +30,7 @@ class StringUserConditionType extends AbstractConditionType implements IDatabase
public function __construct(
public readonly string $identifier,
public readonly string $columnName,
+ public readonly string $category,
public readonly ?string $migrateKeyName = null,
public readonly ?string $migrateConditionObjectType = null,
) {}
@@ -107,6 +108,12 @@ private function getConditions(): array
];
}
+ #[\Override]
+ public function getCategory(): string
+ {
+ return $this->category;
+ }
+
#[\Override]
public function canMigrateConditionData(string $objectType): bool
{
diff --git a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss
index 2ef8fb1420..02feb3d11d 100644
--- a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss
+++ b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss
@@ -14,6 +14,28 @@
}
}
+.scrollableCheckboxList__category:not(:first-child) {
+ margin-top: 5px;
+}
+
+.scrollableCheckboxList__category__label {
+ align-items: center;
+ color: var(--wcfContentDimmedText);
+ column-gap: 10px;
+ display: flex;
+ font-size: 12px;
+ margin-bottom: 5px;
+ white-space: nowrap;
+
+ &::after {
+ border-top: 1px solid currentColor;
+ content: "";
+ display: block;
+ width: 100%;
+ opacity: 0.34;
+ }
+}
+
.dialogContent .scrollableCheckboxList {
max-height: 300px;
}
diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml
index f9d817fdfb..3fb5b6c8e0 100644
--- a/wcfsetup/install/lang/de.xml
+++ b/wcfsetup/install/lang/de.xml
@@ -3570,6 +3570,10 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt
+
+
+
+
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml
index bd2e8c60d7..3be6ec4e53 100644
--- a/wcfsetup/install/lang/en.xml
+++ b/wcfsetup/install/lang/en.xml
@@ -3493,6 +3493,10 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi
+
+
+
+