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 + + + +