From ecae817a92060d78819c7a0d6e0d16ca10b64286 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 15 Jul 2025 10:23:39 +0200 Subject: [PATCH 01/14] Migrate notices to new condition system --- .../templates/shared_timeFormField.tpl | 11 + .../update_com.woltlab.wcf_6.3_step1.php | 5 + .../install/files/acp/templates/noticeAdd.tpl | 191 +--------- .../lib/acp/form/NoticeAddForm.class.php | 337 ++++-------------- .../lib/acp/form/NoticeEditForm.class.php | 174 +-------- .../form/UserGroupAssignmentAddForm.class.php | 2 +- .../files/lib/data/notice/Notice.class.php | 11 +- .../lib/data/notice/NoticeAction.class.php | 33 +- .../AbstractConditionProvider.class.php | 4 + .../RequestConditionProvider.class.php | 27 ++ .../provider/UserConditionProvider.class.php | 3 +- .../CombinedConditionProvider.class.php | 33 ++ .../NoticeConditionProvider.class.php | 28 ++ .../type/IGlobalConditionType.class.php | 20 ++ .../ActivePageRequestConditionType.class.php | 49 +++ .../NotOnPageRequestConditionType.class.php | 49 +++ .../TimeRequestConditionType.class.php | 106 ++++++ .../user/IntegerUserConditionType.class.php | 2 +- ...egistrationDateUserConditionType.class.php | 2 +- ...egistrationDaysUserConditionType.class.php | 2 +- .../user/StringUserConditionType.class.php | 2 +- .../ConditionFormContainer.class.php | 3 +- ...refixConditionFormFieldContainer.class.php | 3 +- .../RowConditionFormFieldContainer.class.php | 83 +++++ .../builder/field/TimeFormField.class.php | 124 +++++++ .../lib/system/notice/NoticeHandler.class.php | 13 +- wcfsetup/install/lang/de.xml | 3 + wcfsetup/install/lang/en.xml | 3 + wcfsetup/setup/db/install.sql | 4 +- 29 files changed, 690 insertions(+), 637 deletions(-) create mode 100644 com.woltlab.wcf/templates/shared_timeFormField.tpl create mode 100644 wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php rename wcfsetup/install/files/lib/system/form/builder/container/{ => condition}/ConditionFormContainer.class.php (97%) rename wcfsetup/install/files/lib/system/form/builder/container/{ => condition}/PrefixConditionFormFieldContainer.class.php (98%) create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/condition/RowConditionFormFieldContainer.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php diff --git a/com.woltlab.wcf/templates/shared_timeFormField.tpl b/com.woltlab.wcf/templates/shared_timeFormField.tpl new file mode 100644 index 00000000000..5dc2a443e0b --- /dev/null +++ b/com.woltlab.wcf/templates/shared_timeFormField.tpl @@ -0,0 +1,11 @@ +getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{if $field->isAutofocused()} autofocus{/if}{* + *}{if $field->isRequired()} required{/if}{* + *}{if $field->isImmutable()} disabled{/if}{* + *}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* +*}> diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php index 1a24a49c9b2..cf7a3517cd6 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php @@ -18,4 +18,9 @@ MediumtextDatabaseTableColumn::create('conditions'), DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), ]), + PartialDatabaseTable::create('wcf1_notice') + ->columns([ + MediumtextDatabaseTableColumn::create('conditions'), + DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), + ]), ]; diff --git a/wcfsetup/install/files/acp/templates/noticeAdd.tpl b/wcfsetup/install/files/acp/templates/noticeAdd.tpl index 2df7895077e..853b806781b 100644 --- a/wcfsetup/install/files/acp/templates/noticeAdd.tpl +++ b/wcfsetup/install/files/acp/templates/noticeAdd.tpl @@ -17,195 +17,6 @@ -{include file='shared_formNotice'} - -
-
- -
-
- - {if $errorField == 'noticeName'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.notice.noticeName.error.{$errorType}{/lang} - {/if} - - {/if} -
- - - -
-
- - {if $errorField == 'notice'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {elseif $errorType == 'multilingual'} - {lang}wcf.global.form.error.multilingual{/lang} - {else} - {lang}wcf.acp.notice.notice.error.{$errorType}{/lang} - {/if} - - {/if} -
- - {include file='shared_multipleLanguageInputJavascript' elementIdentifier='notice' forceSelection=false} - -
-
-
- -
-
- -
-
-
- - {lang}wcf.acp.notice.showOrder.description{/lang} -
-
- - {event name='dataFields'} -
- -
-

{lang}wcf.global.settings{/lang}

- -
-
-
- {foreach from=$availableCssClassNames item=className} - - {/foreach} - - - - {if $errorField == 'cssClassName'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.notice.cssClassName.error.{$errorType}{/lang} - {/if} - - {/if} - {lang}wcf.acp.notice.cssClassName.description{/lang} - - {lang}wcf.acp.notice.example{/lang} -
-
- -
-
-
- -
-
- -
-
-
- - {lang}wcf.acp.notice.isDismissible.description{/lang} -
-
- - {if $action == 'edit' && $notice->isDismissible} -
-
-
- - {lang}wcf.acp.notice.resetIsDismissed.description{/lang} -
-
- {/if} - - {event name='settingsFields'} -
- - {event name='sections'} - -
-
-

{lang}wcf.acp.notice.conditions{/lang}

-

{lang}wcf.acp.notice.conditions.description{/lang}

-
- -
-
-

{lang}wcf.acp.notice.conditions.page{/lang}

-

{lang}wcf.acp.notice.conditions.page.description{/lang}

-
- - {foreach from=$groupedConditionObjectTypes['com.woltlab.wcf.page'] item='pageConditionObjectType'} - {unsafe:$pageConditionObjectType->getProcessor()->getHtml()} - {/foreach} -
- -
-
-

{lang}wcf.acp.notice.conditions.pointInTime{/lang}

-

{lang}wcf.acp.notice.conditions.pointInTime.description{/lang}

-
- - {foreach from=$groupedConditionObjectTypes['com.woltlab.wcf.pointInTime'] item='pointInTimeConditionObjectType'} - {unsafe:$pointInTimeConditionObjectType->getProcessor()->getHtml()} - {/foreach} -
- - {event name='conditionTypeSections'} -
- -
-
-

{lang}wcf.acp.notice.conditions.user{/lang}

-

{lang}wcf.acp.notice.conditions.user.description{/lang}

-
- - {include file='shared_userConditions' groupedObjectTypes=$groupedConditionObjectTypes['com.woltlab.wcf.user']} -
- - {event name='conditionContainers'} - -
- - {csrfToken} -
-
- - - +{unsafe:$form->getHtml()} {include file='footer'} diff --git a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php index 241a6de6804..628015d7078 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php @@ -4,308 +4,105 @@ use wcf\data\notice\Notice; use wcf\data\notice\NoticeAction; -use wcf\data\notice\NoticeEditor; -use wcf\data\object\type\ObjectType; -use wcf\data\object\type\ObjectTypeCache; -use wcf\form\AbstractForm; -use wcf\system\condition\ConditionHandler; -use wcf\system\exception\UserInputException; -use wcf\system\language\I18nHandler; -use wcf\system\Regex; -use wcf\system\request\LinkHandler; -use wcf\system\WCF; -use wcf\util\StringUtil; +use wcf\data\notice\NoticeList; +use wcf\form\AbstractFormBuilderForm; +use wcf\system\condition\provider\combined\NoticeConditionProvider; +use wcf\system\form\builder\container\condition\ConditionFormContainer; +use wcf\system\form\builder\container\FormContainer; +use wcf\system\form\builder\field\BooleanFormField; +use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; +use wcf\system\form\builder\field\MultilineTextFormField; +use wcf\system\form\builder\field\ShowOrderFormField; +use wcf\system\form\builder\field\TextFormField; /** * Shows the form to create a new notice. * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Matthias Schmidt + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License + * + * @extends AbstractFormBuilderForm */ -class NoticeAddForm extends AbstractForm +class NoticeAddForm extends AbstractFormBuilderForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.notice.add'; - /** - * name of the chosen CSS class name - * @var string - */ - public $cssClassName = 'info'; - - /** - * custom CSS class name - * @var string - */ - public $customCssClassName = ''; - - /** - * grouped notice condition object types - * @var (ObjectType|ObjectType[])[][] - */ - public $groupedConditionObjectTypes = []; - - /** - * 1 if the notice is disabled - * @var int - */ - public $isDisabled = 0; - - /** - * 1 if the notice is dismissible - * @var int - */ - public $isDismissible = 0; - /** * @inheritDoc */ public $neededPermissions = ['admin.notice.canManageNotice']; - /** - * name of the notice - * @var string - */ - public $noticeName = ''; - - /** - * 1 if html is used in the notice text - * @var int - */ - public $noticeUseHtml = 0; - - /** - * order used to the show the notices - * @var int - */ - public $showOrder = 0; - /** * @inheritDoc */ - public function assignVariables() - { - parent::assignVariables(); - - I18nHandler::getInstance()->assignVariables(); - - WCF::getTPL()->assign([ - 'action' => 'add', - 'availableCssClassNames' => Notice::TYPES, - 'cssClassName' => $this->cssClassName, - 'customCssClassName' => $this->customCssClassName, - 'isDisabled' => $this->isDisabled, - 'isDismissible' => $this->isDismissible, - 'groupedConditionObjectTypes' => $this->groupedConditionObjectTypes, - 'noticeName' => $this->noticeName, - 'noticeUseHtml' => $this->noticeUseHtml, - 'showOrder' => $this->showOrder, - ]); - } + public $objectEditLinkController = NoticeEditForm::class; /** * @inheritDoc */ - public function readData() - { - $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.condition.notice'); - foreach ($objectTypes as $objectType) { - if (!$objectType->conditionobject) { - continue; - } + public $objectActionClass = NoticeAction::class; - if (!isset($this->groupedConditionObjectTypes[$objectType->conditionobject])) { - $this->groupedConditionObjectTypes[$objectType->conditionobject] = []; - } - - if ($objectType->conditiongroup) { - if (!isset($this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->conditiongroup])) { - $this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->conditiongroup] = []; - } - - $this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->conditiongroup][$objectType->objectTypeID] = $objectType; - } else { - $this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->objectTypeID] = $objectType; - } - } - - parent::readData(); - } - - /** - * @inheritDoc - */ - public function readFormParameters() - { - parent::readFormParameters(); - - I18nHandler::getInstance()->readValues(); - - if (isset($_POST['cssClassName'])) { - $this->cssClassName = StringUtil::trim($_POST['cssClassName']); - } - if (isset($_POST['customCssClassName'])) { - $this->customCssClassName = StringUtil::trim($_POST['customCssClassName']); - } - if (isset($_POST['isDisabled'])) { - $this->isDisabled = 1; - } - if (isset($_POST['isDismissible'])) { - $this->isDismissible = 1; - } - if (isset($_POST['noticeName'])) { - $this->noticeName = StringUtil::trim($_POST['noticeName']); - } - if (isset($_POST['noticeUseHtml'])) { - $this->noticeUseHtml = 1; - } - if (isset($_POST['showOrder'])) { - $this->showOrder = \intval($_POST['showOrder']); - } - - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - foreach ($objectTypes as $objectType) { - $objectType->getProcessor()->readFormParameters(); - } - } else { - $objectTypes->getProcessor()->readFormParameters(); - } - } - } - } - - /** - * @inheritDoc - */ - public function readParameters() - { - parent::readParameters(); - - I18nHandler::getInstance()->register('notice'); - } - - /** - * @inheritDoc - */ - public function save() + #[\Override] + protected function createForm() { - parent::save(); - - $this->objectAction = new NoticeAction([], 'create', [ - 'data' => \array_merge($this->additionalFields, [ - 'cssClassName' => $this->cssClassName == 'custom' ? $this->customCssClassName : $this->cssClassName, - 'isDisabled' => $this->isDisabled, - 'isDismissible' => $this->isDismissible, - 'notice' => I18nHandler::getInstance()->isPlainValue('notice') ? I18nHandler::getInstance()->getValue('notice') : '', - 'noticeName' => $this->noticeName, - 'noticeUseHtml' => $this->noticeUseHtml, - 'showOrder' => $this->showOrder, - ]), - ]); - $returnValues = $this->objectAction->executeAction(); - - if (!I18nHandler::getInstance()->isPlainValue('notice')) { - I18nHandler::getInstance()->save( - 'notice', - 'wcf.notice.notice.notice' . $returnValues['returnValues']->noticeID, - 'wcf.notice', - 1 - ); - - // update notice name - $noticeEditor = new NoticeEditor($returnValues['returnValues']); - $noticeEditor->update([ - 'notice' => 'wcf.notice.notice.notice' . $returnValues['returnValues']->noticeID, - ]); - } - - // transform conditions array into one-dimensional array - $conditions = []; - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - $conditions = \array_merge($conditions, $objectTypes); - } else { - $conditions[] = $objectTypes; - } - } - } - - ConditionHandler::getInstance()->createConditions($returnValues['returnValues']->noticeID, $conditions); - - $this->saved(); - - // reset values - $this->cssClassName = ''; - $this->customCssClassName = ''; - $this->isDisabled = 0; - $this->isDismissible = 0; - $this->noticeName = ''; - $this->noticeUseHtml = 0; - $this->showOrder = 0; - I18nHandler::getInstance()->reset(); - - foreach ($conditions as $condition) { - $condition->getProcessor()->reset(); - } - - WCF::getTPL()->assign([ - 'success' => true, - 'objectEditLink' => LinkHandler::getInstance()->getControllerLink( - NoticeEditForm::class, - ['id' => $returnValues['returnValues']->noticeID] - ), + parent::createForm(); + + $this->form->appendChildren([ + FormContainer::create('generalSection') + ->appendChildren([ + TextFormField::create('noticeName') + ->label('wcf.global.name') + ->required(), + MultilineTextFormField::create('notice') + ->i18n() + ->languageItemPattern('wcf.notice.notice.notice\d+') + ->required(), + BooleanFormField::create('noticeUseHtml') + ->label('wcf.acp.notice.noticeUseHtml'), + ShowOrderFormField::create() + ->description('wcf.acp.notice.showOrder.description') + ->options($this->getNotices()), + ]), + FormContainer::create('settingsSection') + ->label('wcf.global.settings') + ->appendChildren([ + /*TODO + * SingleSelectionFormField::create('cssClassName') + ->label('wcf.acp.notice.cssClassName') + ->required(), + */ + BooleanFormField::create('isDisabled') + ->label('wcf.acp.notice.isDisabled'), + BooleanFormField::create('isDismissible') + ->label('wcf.acp.notice.isDismissible') + ->description('wcf.acp.notice.isDismissible.description'), + BooleanFormField::create('resetIsDismissed') + ->label('wcf.acp.notice.resetIsDismissed') + ->description('wcf.acp.notice.resetIsDismissed.description') + ->available($this->formAction === 'edit') + ->addDependency( + NonEmptyFormFieldDependency::create('isDismissibleDependency') + ->fieldId('isDismissible') + ), + ]), + ConditionFormContainer::create() + ->conditionProvider(new NoticeConditionProvider()), ]); } /** - * @inheritDoc + * @return Notice[] */ - public function validate() + private function getNotices(): array { - parent::validate(); - - if (empty($this->noticeName)) { - throw new UserInputException('noticeName'); - } - - if (!I18nHandler::getInstance()->validateValue('notice')) { - if (I18nHandler::getInstance()->isPlainValue('notice')) { - throw new UserInputException('notice'); - } else { - throw new UserInputException('notice', 'multilingual'); - } - } - - // validate class name - if (empty($this->cssClassName)) { - throw new UserInputException('cssClassName'); - } elseif ($this->cssClassName == 'custom') { - if (empty($this->cssClassName)) { - throw new UserInputException('cssClassName'); - } - if (!Regex::compile('^-?[_a-zA-Z]+[_a-zA-Z0-9-]+$')->match($this->customCssClassName)) { - throw new UserInputException('cssClassName', 'invalid'); - } - } elseif (!\in_array($this->cssClassName, Notice::TYPES)) { - throw new UserInputException('cssClassName', 'invalid'); - } + $optionList = new NoticeList(); + $optionList->sqlOrderBy = "showOrder ASC"; + $optionList->readObjects(); - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - foreach ($objectTypes as $objectType) { - $objectType->getProcessor()->validate(); - } - } else { - $objectTypes->getProcessor()->validate(); - } - } - } + return $optionList->getObjects(); } } diff --git a/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php index 06fbead5e83..a4688e322c5 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php @@ -4,16 +4,13 @@ use wcf\acp\page\NoticeListPage; use wcf\data\notice\Notice; -use wcf\data\notice\NoticeAction; -use wcf\form\AbstractForm; -use wcf\system\condition\ConditionHandler; use wcf\system\exception\IllegalLinkException; +use wcf\system\exception\NamedUserException; use wcf\system\interaction\admin\NoticeInteractions; use wcf\system\interaction\StandaloneInteractionContextMenuComponent; -use wcf\system\language\I18nHandler; use wcf\system\request\LinkHandler; -use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; +use wcf\util\HtmlString; /** * Shows the form to edit an existing notice. @@ -29,186 +26,43 @@ class NoticeEditForm extends NoticeAddForm */ public $activeMenuItem = 'wcf.acp.menu.link.notice.list'; - /** - * edited notice object - * @var Notice - */ - public $notice; - - /** - * id of the edited notice object - * @var int - */ - public $noticeID = 0; - - /** - * 1 if the notice will be displayed for all users again - * @var int - */ - public $resetIsDismissed = 0; - /** * @inheritDoc */ + public $formAction = 'edit'; + + #[\Override] public function assignVariables() { parent::assignVariables(); - I18nHandler::getInstance()->assignVariables(!empty($_POST)); - WCF::getTPL()->assign([ 'action' => 'edit', - 'notice' => $this->notice, - 'resetIsDismissed' => $this->resetIsDismissed, 'interactionContextMenu' => StandaloneInteractionContextMenuComponent::forContentHeaderButton( new NoticeInteractions(), - $this->notice, + $this->formObject, LinkHandler::getInstance()->getControllerLink(NoticeListPage::class) ), ]); } - /** - * @inheritDoc - */ - public function readData() - { - parent::readData(); - - if (empty($_POST)) { - I18nHandler::getInstance()->setOptions('notice', 1, $this->notice->notice, 'wcf.notice.notice.notice\d+'); - - $this->cssClassName = $this->notice->cssClassName; - if (!\in_array($this->cssClassName, Notice::TYPES)) { - $this->customCssClassName = $this->cssClassName; - $this->cssClassName = 'custom'; - } - - $this->isDisabled = $this->notice->isDisabled; - $this->isDismissible = $this->notice->isDismissible; - $this->noticeName = $this->notice->noticeName; - $this->noticeUseHtml = $this->notice->noticeUseHtml; - $this->showOrder = $this->notice->showOrder; - - $conditions = $this->notice->getConditions(); - $conditionsByObjectTypeID = []; - foreach ($conditions as $condition) { - $conditionsByObjectTypeID[$condition->objectTypeID] = $condition; - } - - foreach ($this->groupedConditionObjectTypes as $objectTypes1) { - foreach ($objectTypes1 as $objectTypes2) { - if (\is_array($objectTypes2)) { - foreach ($objectTypes2 as $objectType) { - if (isset($conditionsByObjectTypeID[$objectType->objectTypeID])) { - $conditionsByObjectTypeID[$objectType->objectTypeID]->getObjectType()->getProcessor()->setData($conditionsByObjectTypeID[$objectType->objectTypeID]); - } - } - } elseif (isset($conditionsByObjectTypeID[$objectTypes2->objectTypeID])) { - $conditionsByObjectTypeID[$objectTypes2->objectTypeID]->getObjectType()->getProcessor()->setData($conditionsByObjectTypeID[$objectTypes2->objectTypeID]); - } - } - } - } - } - - /** - * @inheritDoc - */ - public function readFormParameters() - { - parent::readFormParameters(); - - if (isset($_POST['resetIsDismissed'])) { - $this->resetIsDismissed = 1; - } - } - - /** - * @inheritDoc - */ + #[\Override] public function readParameters() { parent::readParameters(); - if (isset($_REQUEST['id'])) { - $this->noticeID = \intval($_REQUEST['id']); + if (!isset($_REQUEST['id'])) { + throw new IllegalLinkException(); } - $this->notice = new Notice($this->noticeID); - if (!$this->notice->noticeID) { + $this->formObject = new Notice(\intval($_REQUEST['id'])); + if (!$this->formObject->noticeID) { throw new IllegalLinkException(); } - } - - /** - * @inheritDoc - */ - public function save() - { - AbstractForm::save(); - - $this->objectAction = new NoticeAction([$this->notice], 'update', [ - 'data' => \array_merge($this->additionalFields, [ - 'cssClassName' => $this->cssClassName == 'custom' ? $this->customCssClassName : $this->cssClassName, - 'isDisabled' => $this->isDisabled, - 'isDismissible' => $this->isDismissible, - 'notice' => I18nHandler::getInstance()->isPlainValue('notice') ? I18nHandler::getInstance()->getValue('notice') : 'wcf.notice.notice.notice' . $this->notice->noticeID, - 'noticeName' => $this->noticeName, - 'noticeUseHtml' => $this->noticeUseHtml, - 'showOrder' => $this->showOrder, - ]), - ]); - $this->objectAction->executeAction(); - if (I18nHandler::getInstance()->isPlainValue('notice')) { - if ($this->notice->notice == 'wcf.notice.notice.notice' . $this->notice->noticeID) { - I18nHandler::getInstance()->remove($this->notice->notice); - } - } else { - I18nHandler::getInstance()->save( - 'notice', - 'wcf.notice.notice.notice' . $this->notice->noticeID, - 'wcf.notice', - 1 + if ($this->formObject->isLegacy) { + throw new NamedUserException( + HtmlString::fromSafeHtml(WCF::getLanguage()->getDynamicVariable('wcf.acp.notice.legacyNotice')) // TODO add language item ); } - - // transform conditions array into one-dimensional array - $conditions = []; - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - $conditions = \array_merge($conditions, $objectTypes); - } else { - $conditions[] = $objectTypes; - } - } - } - - ConditionHandler::getInstance()->updateConditions( - $this->notice->noticeID, - $this->notice->getConditions(), - $conditions - ); - - if ($this->resetIsDismissed) { - $sql = "DELETE FROM wcf1_notice_dismissed - WHERE noticeID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([ - $this->notice->noticeID, - ]); - - $this->resetIsDismissed = 0; - - UserStorageHandler::getInstance()->resetAll('dismissedNotices'); - } - - $this->saved(); - - // reload notice object for proper 'isDismissible' value - $this->notice = new Notice($this->noticeID); - - WCF::getTPL()->assign('success', true); } } diff --git a/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php index 6929fa248ce..133b6c2da3a 100644 --- a/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php @@ -7,7 +7,7 @@ use wcf\data\user\group\UserGroup; use wcf\form\AbstractFormBuilderForm; use wcf\system\condition\provider\UserConditionProvider; -use wcf\system\form\builder\container\ConditionFormContainer; +use wcf\system\form\builder\container\condition\ConditionFormContainer; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\field\BooleanFormField; use wcf\system\form\builder\field\SingleSelectionFormField; diff --git a/wcfsetup/install/files/lib/data/notice/Notice.class.php b/wcfsetup/install/files/lib/data/notice/Notice.class.php index c2eedc27023..fed6c8aad39 100644 --- a/wcfsetup/install/files/lib/data/notice/Notice.class.php +++ b/wcfsetup/install/files/lib/data/notice/Notice.class.php @@ -2,12 +2,11 @@ namespace wcf\data\notice; -use wcf\data\condition\Condition; use wcf\data\DatabaseObject; -use wcf\system\condition\ConditionHandler; use wcf\system\request\IRouteController; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; +use wcf\util\JSON; use wcf\util\StringUtil; /** @@ -25,6 +24,8 @@ * @property-read int $showOrder position of the notice in relation to the other notices * @property-read int $isDisabled is `1` if the notice is disabled and thus not shown, otherwise `0` * @property-read int $isDismissible is `1` if the notice can be dismissed by users, otherwise `0` + * @property-read string $conditions + * @property-read int $isLegacy */ class Notice extends DatabaseObject implements IRouteController { @@ -65,11 +66,11 @@ public function __toString(): string /** * Returns the conditions of the notice. * - * @return Condition[] + * @return array{identifier: string, value: mixed}[] */ - public function getConditions() + public function getConditions(): array { - return ConditionHandler::getInstance()->getConditions('com.woltlab.wcf.condition.notice', $this->noticeID); + return JSON::decode($this->conditions); } /** diff --git a/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php b/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php index 325174eea6c..26818eb10b5 100644 --- a/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php +++ b/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php @@ -5,6 +5,7 @@ use wcf\data\AbstractDatabaseObjectAction; use wcf\data\IToggleAction; use wcf\data\TDatabaseObjectToggle; +use wcf\data\TI18nDatabaseObjectAction; use wcf\system\condition\ConditionHandler; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; @@ -21,6 +22,7 @@ class NoticeAction extends AbstractDatabaseObjectAction implements IToggleAction { use TDatabaseObjectToggle; + use TI18nDatabaseObjectAction; /** * @inheritDoc @@ -56,6 +58,9 @@ public function create() /** @var Notice $notice */ $notice = parent::create(); + + $this->saveI18nValue($notice); + $noticeEditor = new NoticeEditor($notice); $noticeEditor->setShowOrder($showOrder); @@ -69,7 +74,11 @@ public function delete() { ConditionHandler::getInstance()->deleteConditions('com.woltlab.wcf.condition.notice', $this->objectIDs); - return parent::delete(); + $count = parent::delete(); + + $this->deleteI18nValues(); + + return $count; } /** @@ -126,6 +135,10 @@ public function update() { parent::update(); + foreach ($this->getObjects() as $labelEditor) { + $this->saveI18nValue($labelEditor->getDecoratedObject()); + } + if ( \count($this->objects) == 1 && isset($this->parameters['data']['showOrder']) @@ -134,4 +147,22 @@ public function update() \reset($this->objects)->setShowOrder($this->parameters['data']['showOrder']); } } + + #[\Override] + public function getI18nSaveTypes(): array + { + return ['notice' => 'wcf.notice.notice.notice\d+']; + } + + #[\Override] + public function getLanguageCategory(): string + { + return 'wcf.notice'; + } + + #[\Override] + public function getPackageID(): int + { + return PACKAGE_ID; + } } diff --git a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php index 1d26c45869e..f3ee4f3879d 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php @@ -27,6 +27,10 @@ abstract class AbstractConditionProvider */ public function addCondition(IConditionType $conditionType): void { + if (\array_key_exists($conditionType->getIdentifier(), $this->conditionTypes)) { + throw new \InvalidArgumentException("Condition type with identifier '{$conditionType->getIdentifier()}' already exists."); + } + $this->conditionTypes[$conditionType->getIdentifier()] = $conditionType; } diff --git a/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php new file mode 100644 index 00000000000..48754c98d51 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php @@ -0,0 +1,27 @@ + + * @since 6.3 + * + * @phpstan-type RequestConditionType IGlobalConditionType + * @extends AbstractConditionProvider + */ +final class RequestConditionProvider extends AbstractConditionProvider +{ + public function __construct() + { + $this->addCondition(new TimeRequestConditionType()); + $this->addCondition(new ActivePageRequestConditionType()); + $this->addCondition(new NotOnPageRequestConditionType()); + } +} 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 1e622a03036..6ac0a8792bc 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php @@ -28,7 +28,8 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @extends AbstractConditionProvider&IObjectConditionType> + * @phpstan-type UserConditionType IDatabaseObjectListConditionType&IObjectConditionType + * @extends AbstractConditionProvider */ final class UserConditionProvider extends AbstractConditionProvider { diff --git a/wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php new file mode 100644 index 00000000000..c0174240927 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php @@ -0,0 +1,33 @@ + + * @since 6.3 + * + * @template TConditionType of IConditionType + * @template TConditionProvider of AbstractConditionProvider + * @extends AbstractConditionProvider + */ +abstract class CombinedConditionProvider extends AbstractConditionProvider +{ + /** + * @param TConditionProvider ...$providers + */ + public function __construct(AbstractConditionProvider ...$providers) + { + foreach ($providers as $provider) { + foreach ($provider->getConditionTypes() as $condition) { + $this->addCondition($condition); + } + } + } +} diff --git a/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php new file mode 100644 index 00000000000..44de926890f --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php @@ -0,0 +1,28 @@ + + * @since 6.3 + * + * @phpstan-import-type RequestConditionType from RequestConditionProvider + * @phpstan-import-type UserConditionType from UserConditionProvider + * @extends CombinedConditionProvider|AbstractConditionProvider> + */ +final class NoticeConditionProvider extends CombinedConditionProvider +{ + public function __construct() + { + parent::__construct( + new UserConditionProvider(), + new RequestConditionProvider(), + ); + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php new file mode 100644 index 00000000000..ee7e3cd28e3 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php @@ -0,0 +1,20 @@ + + * @since 6.3 + * + * @template TFilter + * @extends IConditionType + */ +interface IGlobalConditionType extends IConditionType +{ + /** + * Returns `true` if the condition matches the global context (e.g., the active user via `WCF::getUser()`). + */ + public function matches(): bool; +} 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 new file mode 100644 index 00000000000..3fc78d841f3 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php @@ -0,0 +1,49 @@ + + * @since 6.3 + * + * @implements IGlobalConditionType + * @extends AbstractConditionType + */ +final class ActivePageRequestConditionType extends AbstractConditionType implements IGlobalConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'activePage'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.activePage"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `matches` method. + return SingleSelectionFormField::create($id) + ->options((new PageNodeTree())->getNodeList(), true) + ->required(); + } + + #[\Override] + public function matches(): bool + { + return RequestHandler::getInstance()->getActivePageID() === (int)$this->filter; + } +} 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 new file mode 100644 index 00000000000..552df9227a4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php @@ -0,0 +1,49 @@ + + * @since 6.3 + * + * @implements IGlobalConditionType + * @extends AbstractConditionType + */ +final class NotOnPageRequestConditionType extends AbstractConditionType implements IGlobalConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'notOnPage'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.notOnPage"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `matches` method. + return SingleSelectionFormField::create($id) + ->options((new PageNodeTree())->getNodeList(), true) + ->required(); + } + + #[\Override] + public function matches(): bool + { + return RequestHandler::getInstance()->getActivePageID() !== (int)$this->filter; + } +} 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 new file mode 100644 index 00000000000..8774bad8c87 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php @@ -0,0 +1,106 @@ + + * @since 6.3 + * + * @phpstan-type Filter = array{Condition: string, Value: string, Timezone: string} + * @implements IGlobalConditionType + * @extends AbstractConditionType + */ +final class TimeRequestConditionType extends AbstractConditionType implements IGlobalConditionType +{ + public const USER_TIMEZONE = 'userTimezone'; + + #[\Override] + public function getIdentifier(): string + { + return 'time'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.time"; + } + + #[\Override] + public function getFormField(string $id): RowConditionFormFieldContainer + { + return RowConditionFormFieldContainer::create($id) + ->appendChildren([ + SingleSelectionFormField::create("{$id}Condition") + ->options(\array_combine($this->getConditions(), $this->getConditions())) + ->required(), + TimeFormField::create("{$id}Value") + ->required(), + SingleSelectionFormField::create("{$id}Timezone") + ->options($this->getTimezones()) + ->required(), + ]); + } + + #[\Override] + public function matches(): bool + { + ["Condition" => $condition, "Value" => $time, "Timezone" => $timezone] = $this->filter; + if ($timezone === self::USER_TIMEZONE) { + $timezoneObject = WCF::getUser()->getTimezone(); + } else { + $timezoneObject = new \DateTimeZone($timezone); + } + + $dateTime = \DateTimeImmutable::createFromFormat('H:i', $time, $timezoneObject); + + return match ($condition) { + '>' => TIME_NOW > $dateTime->getTimestamp(), + '<' => TIME_NOW < $dateTime->getTimestamp(), + '>=' => TIME_NOW >= $dateTime->getTimestamp(), + '<=' => TIME_NOW <= $dateTime->getTimestamp(), + default => false, + }; + } + + /** + * @return array + */ + public function getTimezones(): array + { + return \array_merge( + [self::USER_TIMEZONE => 'wcf.date.timezone.user'], + \array_combine( + DateUtil::getAvailableTimezones(), + \array_map( + static fn (string $timezone): string => WCF::getLanguage()->get( + 'wcf.date.timezone.' . \str_replace( + '/', + '.', + \strtolower($timezone) + ) + ), + DateUtil::getAvailableTimezones() + ) + ) + ); + } + + /** + * @return string[] + */ + protected function getConditions(): array + { + return [">", "<", ">=", "<="]; + } +} 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 196d520968f..5b427ff3a4a 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 @@ -9,7 +9,7 @@ use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\IntegerFormField; use wcf\system\form\builder\field\SingleSelectionFormField; 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 80b3f8a95a0..717d5b0fcc9 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 @@ -9,7 +9,7 @@ use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\DateFormField; use wcf\system\form\builder\field\SingleSelectionFormField; 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 c0e2cfe97f1..febacbd506a 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 @@ -3,7 +3,7 @@ namespace wcf\system\condition\type\user; use wcf\data\DatabaseObjectList; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\IntegerFormField; use wcf\util\DateUtil; 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 9093decab28..d3f6d022b1e 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 @@ -9,7 +9,7 @@ use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\form\builder\field\TextFormField; use wcf\system\WCF; diff --git a/wcfsetup/install/files/lib/system/form/builder/container/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php similarity index 97% rename from wcfsetup/install/files/lib/system/form/builder/container/ConditionFormContainer.class.php rename to wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index dad15b6b305..7a2ef60c2ed 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -1,9 +1,10 @@ + * @since 6.3 + */ +final class RowConditionFormFieldContainer extends RowFormFieldContainer +{ + #[\Override] + public function populate() + { + $this->getDocument()->getDataHandler() + ->addProcessor( + new CustomFormDataProcessor( + $this->getId() . "DataProcessor", + function (IFormDocument $document, array $parameters) { + $data = []; + + foreach ($this->children() as $child) { + if (!($child instanceof IFormField)) { + continue; + } + + $id = $child->getId(); + $name = $this->getName($child); + + $data[$name] = $parameters['data'][$id]; + unset($parameters['data'][$id]); + } + + if ($data !== []) { + $parameters['data'][$this->getId()] = $data; + } + + return $parameters; + }, + ) + ); + + return parent::populate(); + } + + #[\Override] + public function updatedObject(array $data, IStorableObject $object, $loadValues = true) + { + if ($loadValues && isset($data[$this->getId()])) { + $values = []; + foreach ($data[$this->getId()] as $name => $value) { + $values[$this->getId() . $name] = $value; + } + + foreach ($this->children() as $child) { + if ($child instanceof IFormField || $child instanceof IFormContainer) { + $child->updatedObject($values, $object, $loadValues); + } + } + } + + return $this; + } + + private function getName(IFormField|IFormContainer $child): string + { + $id = $child->getId(); + + return \mb_substr($id, \mb_strlen($this->getId())); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php new file mode 100644 index 00000000000..9be90a48d5e --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php @@ -0,0 +1,124 @@ + + * @since 6.3 + */ +final class TimeFormField extends AbstractFormField implements + IAttributeFormField, + IAutoFocusFormField, + ICssClassFormField, + IImmutableFormField, + INullableFormField +{ + use TInputAttributeFormField; + use TAutoFocusFormField; + use TCssClassFormField; + use TImmutableFormField; + use TNullableFormField; + + public const FORMAT = 'H:i'; + /** + * @inheritDoc + */ + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Date'; + /** + * @inheritDoc + */ + protected $templateName = 'shared_timeFormField'; + + public function __construct() + { + $this->addFieldClass('medium') + // If no value is set for the time, the time selection cannot be used. + ->value('00:00'); + } + + #[\Override] + public function getSaveValue() + { + if ($this->getValue() === null) { + if ($this->isNullable()) { + return; + } else { + return DateUtil::getDateTimeByTimestamp(0)->format(self::FORMAT); + } + } + + return $this->getValueDateTimeObject()->format(self::FORMAT); + } + + #[\Override] + public function validate() + { + if ($this->getValue() === null) { + if ($this->isRequired()) { + $this->addValidationError(new FormFieldValidationError('empty')); + } + } + } + + #[\Override] + public function value($value) + { + parent::value($value); + + $dateTime = \DateTimeImmutable::createFromFormat( + self::FORMAT, + $this->getValue(), + ); + if ($dateTime === false) { + throw new \InvalidArgumentException( + "Given value does not match format '" . self::FORMAT . "' for field '{$this->getId()}'." + ); + } + + parent::value($dateTime->format(self::FORMAT)); + + return $this; + } + + #[\Override] + public function readValue() + { + if ( + $this->getDocument()->hasRequestData($this->getPrefixedId()) + && \is_string($this->getDocument()->getRequestData($this->getPrefixedId())) + ) { + $value = $this->getDocument()->getRequestData($this->getPrefixedId()); + $this->value = $value; + + if ($this->value === '') { + $this->value = null; + } elseif ($this->getValueDateTimeObject() === null) { + try { + $this->value($value); + } catch (\InvalidArgumentException) { + $this->value = null; + } + } + } + + return $this; + } + + private function getValueDateTimeObject(): ?\DateTimeImmutable + { + $dateTime = \DateTimeImmutable::createFromFormat('H:i', $this->getValue()); + + if ($dateTime === false) { + return null; + } + + return $dateTime; + } +} diff --git a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php index f8eb13fc4cb..aa37d21d675 100644 --- a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php +++ b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php @@ -4,7 +4,11 @@ use wcf\data\notice\Notice; use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\condition\ConditionHandler; +use wcf\system\condition\provider\combined\NoticeConditionProvider; +use wcf\system\condition\type\IGlobalConditionType; use wcf\system\SingletonFactory; +use wcf\system\WCF; /** * Handles notice-related matters. @@ -47,14 +51,19 @@ public function getVisibleNotices() } $notices = []; + $provider = new NoticeConditionProvider(); foreach ($this->notices as $notice) { if ($notice->isDismissed()) { continue; } - $conditions = $notice->getConditions(); + $conditions = ConditionHandler::getInstance()->getConditionsWithFilter($provider, $notice->getConditions()); foreach ($conditions as $condition) { - if (!$condition->getObjectType()->getProcessor()->showContent($condition)) { + $matches = $condition instanceof IGlobalConditionType + ? $condition->matches() + : $condition->matches(WCF::getUser()); + + if (!$matches) { continue 2; } } diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 69319c89e62..cc2be1f930e 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -3562,6 +3562,9 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index e7d1bff1f71..08c4e2e1a52 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3485,6 +3485,9 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 1eb3e7d493e..4bf96a81fae 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -903,7 +903,9 @@ CREATE TABLE wcf1_notice ( cssClassName VARCHAR(255) NOT NULL DEFAULT 'info', showOrder INT(10) NOT NULL DEFAULT 0, isDisabled TINYINT(1) NOT NULL DEFAULT 0, - isDismissible TINYINT(1) NOT NULL DEFAULT 0 + isDismissible TINYINT(1) NOT NULL DEFAULT 0, + conditions MEDIUMTEXT, + isLegacy TINYINT(1) NOT NULL DEFAULT 0 ); DROP TABLE IF EXISTS wcf1_notice_dismissed; From 23808ede76b8f9430a9e1232ed1a8daed0547cd8 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 15 Jul 2025 13:02:00 +0200 Subject: [PATCH 02/14] Add DayOfWeek and NotDayOfWeek condition types --- .../RequestConditionProvider.class.php | 4 ++ .../DayOfWeekRequestConditionType.class.php | 54 +++++++++++++++++++ ...NotDayOfWeekRequestConditionType.class.php | 54 +++++++++++++++++++ wcfsetup/install/lang/de.xml | 2 + wcfsetup/install/lang/en.xml | 2 + 5 files changed, 116 insertions(+) create mode 100644 wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php create mode 100644 wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php diff --git a/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php index 48754c98d51..238d349cb07 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php @@ -4,6 +4,8 @@ use wcf\system\condition\type\IGlobalConditionType; use wcf\system\condition\type\request\ActivePageRequestConditionType; +use wcf\system\condition\type\request\DayOfWeekRequestConditionType; +use wcf\system\condition\type\request\NotDayOfWeekRequestConditionType; use wcf\system\condition\type\request\NotOnPageRequestConditionType; use wcf\system\condition\type\request\TimeRequestConditionType; @@ -23,5 +25,7 @@ public function __construct() $this->addCondition(new TimeRequestConditionType()); $this->addCondition(new ActivePageRequestConditionType()); $this->addCondition(new NotOnPageRequestConditionType()); + $this->addCondition(new DayOfWeekRequestConditionType()); + $this->addCondition(new NotDayOfWeekRequestConditionType()); } } 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 new file mode 100644 index 00000000000..ba41a4cd6d4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php @@ -0,0 +1,54 @@ + + * @since 6.3 + * + * @implements IGlobalConditionType + * @extends AbstractConditionType + */ +final class DayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'dayOfWeek'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.dayOfWeek"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + return SingleSelectionFormField::create($id) + ->options( + \array_map( + static fn ($day) => WCF::getLanguage()->get('wcf.date.day.' . $day), + DateUtil::getWeekDays() + ) + ) + ->required(); + } + + #[\Override] + public function matches(): bool + { + $dateTime = new \DateTimeImmutable("@" . TIME_NOW, WCF::getUser()->getTimeZone()); + + return $dateTime->format('w') === $this->filter; + } +} 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 new file mode 100644 index 00000000000..7e38a2eb3d9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php @@ -0,0 +1,54 @@ + + * @since 6.3 + * + * @implements IGlobalConditionType + * @extends AbstractConditionType + */ +final class NotDayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'notDayOfWeek'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.notDayOfWeek"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + return SingleSelectionFormField::create($id) + ->options( + \array_map( + static fn ($day) => WCF::getLanguage()->get('wcf.date.day.' . $day), + DateUtil::getWeekDays() + ) + ) + ->required(); + } + + #[\Override] + public function matches(): bool + { + $dateTime = new \DateTimeImmutable("@" . TIME_NOW, WCF::getUser()->getTimeZone()); + + return $dateTime->format('w') !== $this->filter; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index cc2be1f930e..902d6dd3c0c 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -3565,6 +3565,8 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 08c4e2e1a52..07edf8320c4 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3488,6 +3488,8 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi + + From b53a584da148582b4a45af0a5a6d1c8824a2ce7b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 15 Jul 2025 13:46:16 +0200 Subject: [PATCH 03/14] Add CSS class name selection field with custom option support --- .../shared_cssClassnameFormField.tpl | 30 ++++ .../lib/acp/form/NoticeAddForm.class.php | 23 ++- .../field/CssClassNameFormField.class.php | 157 ++++++++++++++++++ .../files/style/ui/classNameSelection.scss | 28 ++++ 4 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php create mode 100644 wcfsetup/install/files/style/ui/classNameSelection.scss diff --git a/com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl b/com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl new file mode 100644 index 00000000000..e4486cbe060 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl @@ -0,0 +1,30 @@ +
    + {foreach from=$field->getOptions() key=className item=label} + + + + {/foreach} +
diff --git a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php index 628015d7078..1dfdd799761 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php @@ -10,6 +10,7 @@ use wcf\system\form\builder\container\condition\ConditionFormContainer; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\field\BooleanFormField; +use wcf\system\form\builder\field\CssClassNameFormField; use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; use wcf\system\form\builder\field\MultilineTextFormField; use wcf\system\form\builder\field\ShowOrderFormField; @@ -70,11 +71,13 @@ protected function createForm() FormContainer::create('settingsSection') ->label('wcf.global.settings') ->appendChildren([ - /*TODO - * SingleSelectionFormField::create('cssClassName') + CssClassNameFormField::create('cssClassName') ->label('wcf.acp.notice.cssClassName') + ->visualTemplate('{$label}') + ->description('wcf.acp.notice.cssClassName.description') + ->options($this->getClassNames()) + ->supportCustomClassName() ->required(), - */ BooleanFormField::create('isDisabled') ->label('wcf.acp.notice.isDisabled'), BooleanFormField::create('isDismissible') @@ -105,4 +108,18 @@ private function getNotices(): array return $optionList->getObjects(); } + + /** + * @return array + */ + private function getClassNames(): array + { + $classNames = []; + + foreach (Notice::TYPES as $type) { + $classNames[$type] = 'wcf.acp.notice.cssClassName.' . $type; + } + + return $classNames; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php new file mode 100644 index 00000000000..579b7f8f64f --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php @@ -0,0 +1,157 @@ + + * @since 6.3 + */ +final class CssClassNameFormField extends RadioButtonFormField implements IPatternFormField +{ + use TPatternFormField; + + public const CUSTOM_CSS_CLASSNAME = 'custom'; + + /** + * @inheritDoc + */ + protected $templateName = 'shared_cssClassnameFormField'; + + private string $customClassName = ''; + private string $visualTemplate = '
{$label}
'; + + public function __construct() + { + $this + ->addClass('inlineList') + ->addFieldClass('classNameSelection__radio') + ->pattern('^-?[_a-zA-Z]+[_a-zA-Z0-9-]+$'); + } + + #[\Override] + public function readValue() + { + if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { + $this->value = StringUtil::trim($this->getDocument()->getRequestData($this->getPrefixedId())); + + if ($this->supportsCustomClassName() && $this->value === self::CUSTOM_CSS_CLASSNAME) { + $this->customClassName = StringUtil::trim( + $this->getDocument()->getRequestData($this->getPrefixedId() . 'customCssClassName') + ); + } + } + + return $this; + } + + #[\Override] + public function validate() + { + if ($this->supportsCustomClassName() && $this->getValue() === self::CUSTOM_CSS_CLASSNAME) { + if (!Regex::compile($this->getPattern())->match($this->customClassName)) { + $this->addValidationError( + new FormFieldValidationError( + 'invalid', + 'wcf.global.form.error.invalidCssClassName' + ) + ); + } + } else { + parent::validate(); + } + } + + #[\Override] + public function value($value) + { + if ($this->supportsCustomClassName() && !\array_key_exists($value, $this->options)) { + parent::value(self::CUSTOM_CSS_CLASSNAME); + $this->customClassName = $value; + } else { + parent::value($value); + } + + return $this; + } + + #[\Override] + public function getSaveValue() + { + if ($this->hasCustomClassName()) { + return $this->getCustomClassName(); + } + + return $this->getValue(); + } + + public function hasCustomClassName(): bool + { + return $this->supportsCustomClassName() && $this->value === self::CUSTOM_CSS_CLASSNAME; + } + + public function getCustomClassName(): string + { + return $this->customClassName; + } + + /** + * Sets whether the custom class name is supported. + */ + public function supportCustomClassName(bool $supportCustomClassName = true): self + { + $options = $this->options; + + if ($supportCustomClassName) { + // already supported + if ($this->supportsCustomClassName()) { + return $this; + } + + $options[self::CUSTOM_CSS_CLASSNAME] = ''; + } else { + unset($options[self::CUSTOM_CSS_CLASSNAME]); + } + + return $this->options($options); + } + + /** + * Returns whether the custom class name is supported. + */ + public function supportsCustomClassName(): bool + { + return \array_key_exists(self::CUSTOM_CSS_CLASSNAME, $this->options); + } + + public function visualTemplate(string $visualTemplate): self + { + $this->visualTemplate = $visualTemplate; + + return $this; + } + + public function getVisualTemplate(): string + { + return $this->visualTemplate; + } + + public function renderVisualTemplate(string $className, string $label): string + { + return WCF::getTPL()->fetchString( + WCF::getTPL()->getCompiler()->compileString('visualTemplate', $this->visualTemplate)['template'], + [ + 'className' => $className, + 'label' => $label, + ] + ); + } +} diff --git a/wcfsetup/install/files/style/ui/classNameSelection.scss b/wcfsetup/install/files/style/ui/classNameSelection.scss new file mode 100644 index 00000000000..5c6ed3e58da --- /dev/null +++ b/wcfsetup/install/files/style/ui/classNameSelection.scss @@ -0,0 +1,28 @@ +.classNameSelection.inlineList > li { + flex-basis: 30%; +} + +.classNameSelection > li.custom { + display: flex; +} + +.classNameSelection__label { + display: flex; + flex: 1 1 auto; + align-items: center; + margin-bottom: 10px; +} + +.classNameSelection__radio { + flex: 0 0 auto; + margin-right: 7px; +} + +.classNameSelection > li.custom .classNameSelection__span { + flex: 1 1 auto; +} + +.classNameSelection__label > woltlab-core-notice { + display: inline-flex; + margin-top: 0; +} From ca8d2022d913b7c2c1e997ea1ec59ce2774848c3 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 15 Jul 2025 13:50:07 +0200 Subject: [PATCH 04/14] Add functionality to reset dismissed notices in form --- .../files/lib/acp/form/NoticeAddForm.class.php | 10 ++++++++++ .../lib/acp/form/NoticeEditForm.class.php | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php index 1dfdd799761..39b00b7cb44 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php @@ -9,6 +9,7 @@ use wcf\system\condition\provider\combined\NoticeConditionProvider; use wcf\system\form\builder\container\condition\ConditionFormContainer; use wcf\system\form\builder\container\FormContainer; +use wcf\system\form\builder\data\processor\VoidFormDataProcessor; use wcf\system\form\builder\field\BooleanFormField; use wcf\system\form\builder\field\CssClassNameFormField; use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; @@ -97,6 +98,15 @@ protected function createForm() ]); } + #[\Override] + public function finalizeForm() + { + parent::finalizeForm(); + + $this->form->getDataHandler() + ->addProcessor(new VoidFormDataProcessor('resetIsDismissed')); + } + /** * @return Notice[] */ diff --git a/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php index a4688e322c5..c9a39ceeb25 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php @@ -9,6 +9,7 @@ use wcf\system\interaction\admin\NoticeInteractions; use wcf\system\interaction\StandaloneInteractionContextMenuComponent; use wcf\system\request\LinkHandler; +use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; use wcf\util\HtmlString; @@ -65,4 +66,21 @@ public function readParameters() ); } } + + #[\Override] + public function saved() + { + if ($this->form->getFormField('resetIsDismissed')->getValue()) { + $sql = "DELETE FROM wcf1_notice_dismissed + WHERE noticeID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([ + $this->formObject->noticeID, + ]); + + UserStorageHandler::getInstance()->resetAll('dismissedNotices'); + } + + parent::saved(); + } } From f179e097ef0ad6bbee679975755f9067c1c66c2f Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 16 Jul 2025 08:40:37 +0200 Subject: [PATCH 05/14] Add migration functionality for notice --- com.woltlab.wcf/package.xml | 1 + .../files/acp/templates/noticeList.tpl | 6 +++ .../acp/update_com.woltlab.wcf_6.3_notice.php | 25 ++++++++++ .../lib/acp/page/NoticeListPage.class.php | 22 +++++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../command/MigrateLegacyCondition.class.php | 49 +++++++++++++++++++ .../worker/NoticeRebuildDataWorker.class.php | 39 +++++++++++++++ wcfsetup/install/lang/de.xml | 3 ++ wcfsetup/install/lang/en.xml | 3 ++ 9 files changed, 149 insertions(+) create mode 100644 wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php create mode 100644 wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php create mode 100644 wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index 882df82c685..79930be5e66 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -54,5 +54,6 @@ Required order of the following steps for the update to 6.3: acp/database/update_com.woltlab.wcf_6.3_step1.php acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php + acp/update_com.woltlab.wcf_6.3_notice.php --> diff --git a/wcfsetup/install/files/acp/templates/noticeList.tpl b/wcfsetup/install/files/acp/templates/noticeList.tpl index 5c30518be68..b0e1b98cea0 100644 --- a/wcfsetup/install/files/acp/templates/noticeList.tpl +++ b/wcfsetup/install/files/acp/templates/noticeList.tpl @@ -19,6 +19,12 @@ +{if $hasLegacyObjects} + + {lang}wcf.acp.notice.legacyNotice{/lang} + +{/if} +
{unsafe:$gridView->render()}
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php new file mode 100644 index 00000000000..7d819ee1d51 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php @@ -0,0 +1,25 @@ +exportConditions("com.woltlab.wcf.condition.notice"); +if ($exportedConditions === []) { + return; +} + +$sql = "UPDATE wcf1_notice + SET conditions = ?, + isLegacy = ? + WHERE noticeID = ?"; +$statement = WCF::getDB()->prepare($sql); +foreach ($exportedConditions as $noticeID => $conditionData) { + // TODO handle user option from `com.woltlab.wcf.user.userOptions` + + $statement->execute([ + JSON::encode($conditionData), + 1, + $noticeID, + ]); +} diff --git a/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php b/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php index 6bc2e212bc6..1940cd22639 100644 --- a/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php @@ -4,6 +4,7 @@ use wcf\page\AbstractGridViewPage; use wcf\system\gridView\admin\NoticeGridView; +use wcf\system\WCF; /** * Lists the available notices. @@ -31,4 +32,25 @@ protected function createGridView(): NoticeGridView { return new NoticeGridView(); } + + #[\Override] + public function assignVariables() + { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'hasLegacyObjects' => $this->hasLegacyObjects(), + ]); + } + + private function hasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_notice + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 6fd9e328f4c..1d3acc96c5f 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -101,6 +101,7 @@ static function (\wcf\event\worker\RebuildWorkerCollecting $event) { $event->register(\wcf\system\worker\FileRebuildDataWorker::class, 475); $event->register(\wcf\system\worker\SitemapRebuildWorker::class, 500); $event->register(\wcf\system\worker\UserGroupAssignmentRebuildDataWorker::class, 600); + $event->register(\wcf\system\worker\NoticeRebuildDataWorker::class, 600); $event->register(\wcf\system\worker\StatDailyRebuildDataWorker::class, 800); } ); diff --git a/wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php b/wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php new file mode 100644 index 00000000000..9db92ba3712 --- /dev/null +++ b/wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php @@ -0,0 +1,49 @@ + + * @since 6.3 + */ +final class MigrateLegacyCondition +{ + public function __construct(public readonly Notice $notice) + { + } + + public function __invoke(): void + { + if (!$this->notice->isLegacy) { + return; + } + + try { + $json = JSON::decode($this->notice->conditions); + } catch (SystemException $ex) { + $ex->getExceptionID(); // Log the exception if JSON decoding fails + + return; + } + + $migratedData = ConditionHandler::getInstance()->migrateConditionData(new NoticeConditionProvider(), $json); + + $editor = new NoticeEditor($this->notice); + $editor->update([ + 'conditions' => JSON::encode($migratedData->conditions), + 'isLegacy' => 0, + 'isDisabled' => $migratedData->isFullyMigrated ? $this->notice->isDisabled : 1, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php new file mode 100644 index 00000000000..23a5a1c0936 --- /dev/null +++ b/wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php @@ -0,0 +1,39 @@ + + * @since 6.3 + * + * @extends AbstractLinearRebuildDataWorker + */ +final class NoticeRebuildDataWorker extends AbstractLinearRebuildDataWorker +{ + /** + * @inheritDoc + */ + protected $objectListClassName = NoticeList::class; + + /** + * @inheritDoc + */ + protected $limit = 100; + + #[\Override] + public function execute() + { + parent::execute(); + + foreach ($this->objectList as $notice) { + (new MigrateLegacyCondition($notice))(); + } + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 902d6dd3c0c..f9d817fdfb7 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -1296,6 +1296,7 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE + Anzeigen aktualisieren durch.]]>
@@ -2705,6 +2706,8 @@ Abschnitte dürfen nicht leer sein und nur folgende Zeichen enthalten: [a-z + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 07edf8320c4..bd2e8c60d78 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1272,6 +1272,7 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru + Rebuild Data.]]> @@ -2632,6 +2633,8 @@ If you have already bought the licenses for the listed apps, th + + From 2a8b36b83adf852d00806c796e70aafc61c09153 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 16 Jul 2025 09:20:55 +0200 Subject: [PATCH 06/14] Implement migration support for request condition types --- .../ActivePageRequestConditionType.class.php | 33 +++++++++++- .../DayOfWeekRequestConditionType.class.php | 30 ++++++++++- ...NotDayOfWeekRequestConditionType.class.php | 42 ++++++++++++++- .../NotOnPageRequestConditionType.class.php | 53 ++++++++++++++++++- .../TimeRequestConditionType.class.php | 35 +++++++++++- 5 files changed, 188 insertions(+), 5 deletions(-) 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 3fc78d841f3..cd7f6072180 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 @@ -5,6 +5,7 @@ use wcf\data\page\PageNodeTree; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\request\RequestHandler; @@ -17,7 +18,7 @@ * @implements IGlobalConditionType * @extends AbstractConditionType */ -final class ActivePageRequestConditionType extends AbstractConditionType implements IGlobalConditionType +final class ActivePageRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string @@ -46,4 +47,34 @@ public function matches(): bool { return RequestHandler::getInstance()->getActivePageID() === (int)$this->filter; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $reverseLogic = $conditionData['pageIDs_reverseLogic'] ?? false; + $pageIDs = $conditionData['pageIDs'] ?? []; + + if ($reverseLogic || \count($pageIDs) > 1) { + // `NotOnPageRequestConditionType` should migrate the data. + return []; + } + + $conditions = []; + foreach ($pageIDs as $pageID) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)$pageID, + ]; + } + + unset($conditionData['pageIDs'], $conditionData['pageIDs_reverseLogic']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.page'; + } } 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 ba41a4cd6d4..e8d477ba64e 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 @@ -4,6 +4,7 @@ use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\WCF; use wcf\util\DateUtil; @@ -17,7 +18,7 @@ * @implements IGlobalConditionType * @extends AbstractConditionType */ -final class DayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType +final class DayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string @@ -51,4 +52,31 @@ public function matches(): bool return $dateTime->format('w') === $this->filter; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $daysOfWeeks = $conditionData['daysOfWeek'] ?? []; + if (\count($daysOfWeeks) > 1) { + // `NotDayOfWeekRequestConditionType` should migrate the data. + return []; + } + + $conditions = [ + [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)\reset($daysOfWeeks), + ], + ]; + + unset($conditionData['daysOfWeek']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.pointInTime.daysOfWeek'; + } } 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 7e38a2eb3d9..a02031f2e69 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 @@ -4,6 +4,7 @@ use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\WCF; use wcf\util\DateUtil; @@ -17,7 +18,7 @@ * @implements IGlobalConditionType * @extends AbstractConditionType */ -final class NotDayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType +final class NotDayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string @@ -51,4 +52,43 @@ public function matches(): bool return $dateTime->format('w') !== $this->filter; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $daysOfWeeks = $conditionData['daysOfWeek'] ?? []; + if (\count($daysOfWeeks) <= 1) { + // `DayOfWeekRequestConditionType` should migrate the data. + return []; + } + + if (\count($daysOfWeeks) === 7) { + // If all days have been selected, this condition is unnecessary and can be removed. + unset($conditionData['daysOfWeek']); + + return []; + } + + $conditions = []; + + // We must remove all selected week of days to convert the previous condition from an “or” to an “and” condition. + $daysOfWeeks = \array_diff_key(DateUtil::getWeekDays(), $daysOfWeeks); + + foreach ($daysOfWeeks as $dayOfWeek => $_) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)$dayOfWeek, + ]; + } + + unset($conditionData['daysOfWeek']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.pointInTime.daysOfWeek'; + } } 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 552df9227a4..1b72f95958d 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 @@ -5,8 +5,10 @@ use wcf\data\page\PageNodeTree; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\request\RequestHandler; +use wcf\system\WCF; /** * @author Olaf Braun @@ -17,7 +19,7 @@ * @implements IGlobalConditionType * @extends AbstractConditionType */ -final class NotOnPageRequestConditionType extends AbstractConditionType implements IGlobalConditionType +final class NotOnPageRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string @@ -46,4 +48,53 @@ public function matches(): bool { return RequestHandler::getInstance()->getActivePageID() !== (int)$this->filter; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $reverseLogic = $conditionData['pageIDs_reverseLogic'] ?? false; + $pageIDs = $conditionData['pageIDs'] ?? []; + + if (!$reverseLogic && \count($pageIDs) <= 1) { + // `ActivePageRequestConditionType` should migrate the data. + return []; + } + + $conditions = []; + if (!$reverseLogic) { + // If reverse logic is not activated, we must add all unselected pages. + // This allows us to turn an “or” condition into an “and” condition. + $pageIDs = \array_diff($this->getPageIDs(), $pageIDs); + } + + foreach ($pageIDs as $pageID) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)$pageID, + ]; + } + + unset($conditionData['pageIDs'], $conditionData['pageIDs_reverseLogic']); + + return $conditions; + } + + /** + * @return int[] + */ + private function getPageIDs(): array + { + $sql = "SELECT pageID + FROM wcf1_page"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute(); + + return $statement->fetchAll(\PDO::FETCH_COLUMN); + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.page'; + } } 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 8774bad8c87..914a954a526 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 @@ -4,6 +4,7 @@ use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\container\condition\RowConditionFormFieldContainer; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\form\builder\field\TimeFormField; @@ -20,7 +21,7 @@ * @implements IGlobalConditionType * @extends AbstractConditionType */ -final class TimeRequestConditionType extends AbstractConditionType implements IGlobalConditionType +final class TimeRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType { public const USER_TIMEZONE = 'userTimezone'; @@ -103,4 +104,36 @@ protected function getConditions(): array { return [">", "<", ">=", "<="]; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $startTime = $conditionData['startTime'] ?? null; + $endTime = $conditionData['endTime'] ?? null; + $timezone = $conditionData['timezone'] ?? self::USER_TIMEZONE; + $conditions = []; + + if ($startTime !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => ["Value" => $startTime, 'Condition' => '>', 'Timezone' => $timezone], + ]; + } + if ($endTime !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => ["Value" => $endTime, 'Condition' => '<', 'Timezone' => $timezone], + ]; + } + + unset($conditionData['startTime'], $conditionData['endTime'], $conditionData['timezone']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.pointInTime.time'; + } } From 82b5e71fd8077d31ebf740d1f92f37fc48fd8779 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 16 Jul 2025 09:25:33 +0200 Subject: [PATCH 07/14] Migrate `NoticeCacheBuilder` to an eager cache --- .../lib/data/notice/NoticeEditor.class.php | 11 ++----- .../builder/NoticeCacheBuilder.class.php | 23 +++++++------- .../system/cache/eager/NoticeCache.class.php | 31 +++++++++++++++++++ .../core/notices/ChangeShowOrder.class.php | 5 ++- .../lib/system/notice/NoticeHandler.class.php | 4 +-- 5 files changed, 49 insertions(+), 25 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php diff --git a/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php b/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php index c1baac8bf72..194237f9446 100644 --- a/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php +++ b/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php @@ -4,9 +4,7 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; -use wcf\data\object\type\ObjectTypeCache; -use wcf\system\cache\builder\ConditionCacheBuilder; -use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\cache\eager\NoticeCache; use wcf\system\WCF; /** @@ -67,11 +65,6 @@ public function setShowOrder($showOrder = 0) */ public static function resetCache() { - NoticeCacheBuilder::getInstance()->reset(); - ConditionCacheBuilder::getInstance()->reset([ - 'definitionID' => ObjectTypeCache::getInstance() - ->getDefinitionByName('com.woltlab.wcf.condition.notice') - ->definitionID, - ]); + (new NoticeCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php index fc0cf38f6fd..9f918197a42 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php @@ -2,7 +2,7 @@ namespace wcf\system\cache\builder; -use wcf\data\notice\NoticeList; +use wcf\system\cache\eager\NoticeCache; /** * Caches the enabled notices. @@ -10,19 +10,20 @@ * @author Matthias Schmidt * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 use `NoticeCache` instead */ -class NoticeCacheBuilder extends AbstractCacheBuilder +final class NoticeCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - protected function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $noticeList = new NoticeList(); - $noticeList->getConditionBuilder()->add('isDisabled = ?', [0]); - $noticeList->sqlOrderBy = 'showOrder ASC'; - $noticeList->readObjects(); + return (new NoticeCache())->getCache(); + } - return $noticeList->getObjects(); + #[\Override] + public function reset(array $parameters = []) + { + (new NoticeCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php new file mode 100644 index 00000000000..6dc16dddf94 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + * + * @extends AbstractEagerCache> + */ +final class NoticeCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): array + { + $noticeList = new NoticeList(); + $noticeList->getConditionBuilder()->add('isDisabled = ?', [0]); + $noticeList->getConditionBuilder()->add('isLegacy = ?', [0]); + $noticeList->sqlOrderBy = 'showOrder ASC'; + $noticeList->readObjects(); + + return $noticeList->getObjects(); + } +} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php index 3d4031c2d32..057694261ae 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php @@ -6,9 +6,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use wcf\data\notice\Notice; -use wcf\data\notice\NoticeCache; use wcf\data\notice\NoticeList; -use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\cache\eager\NoticeCache; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; use wcf\system\showOrder\ShowOrderHandler; @@ -63,6 +62,6 @@ private function saveShowOrder(array $items): void } WCF::getDB()->commitTransaction(); - NoticeCacheBuilder::getInstance()->reset(); + (new NoticeCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php index aa37d21d675..56d966cc5b6 100644 --- a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php +++ b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php @@ -3,7 +3,7 @@ namespace wcf\system\notice; use wcf\data\notice\Notice; -use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\cache\eager\NoticeCache; use wcf\system\condition\ConditionHandler; use wcf\system\condition\provider\combined\NoticeConditionProvider; use wcf\system\condition\type\IGlobalConditionType; @@ -36,7 +36,7 @@ class NoticeHandler extends SingletonFactory */ protected function init() { - $this->notices = NoticeCacheBuilder::getInstance()->getData(); + $this->notices = (new NoticeCache())->getCache(); } /** From 40f1b8941358480aced8651435d93c21279c8b56 Mon Sep 17 00:00:00 2001 From: Olaf Braun Date: Wed, 16 Jul 2025 13:07:42 +0200 Subject: [PATCH 08/14] Update wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php Co-authored-by: Alexander Ebert --- .../type/request/NotDayOfWeekRequestConditionType.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a02031f2e69..50fddc3b25d 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 @@ -74,7 +74,7 @@ public function migrateConditionData(array &$conditionData): array // We must remove all selected week of days to convert the previous condition from an “or” to an “and” condition. $daysOfWeeks = \array_diff_key(DateUtil::getWeekDays(), $daysOfWeeks); - foreach ($daysOfWeeks as $dayOfWeek => $_) { + foreach (\array_keys($daysOfWeeks) as $dayOfWeek) { $conditions[] = [ 'identifier' => $this->getIdentifier(), 'value' => (string)$dayOfWeek, From 32a880ceb5fddbe71995f7d60824407d14d645b9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 16 Jul 2025 13:08:44 +0200 Subject: [PATCH 09/14] Use `1` instead of `PACKAGE_ID`, to ensure that the correct package ID is set --- wcfsetup/install/files/lib/data/notice/NoticeAction.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php b/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php index 26818eb10b5..c9795c68c4b 100644 --- a/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php +++ b/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php @@ -163,6 +163,6 @@ public function getLanguageCategory(): string #[\Override] public function getPackageID(): int { - return PACKAGE_ID; + return 1; } } From 97be7f56caf6df30be41b31ca2f97089e4c93e3d Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 16 Jul 2025 13:09:05 +0200 Subject: [PATCH 10/14] Rename `IGlobalConditionType` to `IContextualConditionType` --- .../condition/provider/RequestConditionProvider.class.php | 4 ++-- ...ionType.class.php => IContextualConditionType.class.php} | 2 +- .../type/request/ActivePageRequestConditionType.class.php | 6 +++--- .../type/request/DayOfWeekRequestConditionType.class.php | 6 +++--- .../type/request/NotDayOfWeekRequestConditionType.class.php | 6 +++--- .../type/request/NotOnPageRequestConditionType.class.php | 6 +++--- .../type/request/TimeRequestConditionType.class.php | 6 +++--- .../install/files/lib/system/notice/NoticeHandler.class.php | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) rename wcfsetup/install/files/lib/system/condition/type/{IGlobalConditionType.class.php => IContextualConditionType.class.php} (88%) diff --git a/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php index 238d349cb07..d2464147c8d 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php @@ -2,7 +2,7 @@ namespace wcf\system\condition\provider; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\request\ActivePageRequestConditionType; use wcf\system\condition\type\request\DayOfWeekRequestConditionType; use wcf\system\condition\type\request\NotDayOfWeekRequestConditionType; @@ -15,7 +15,7 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @phpstan-type RequestConditionType IGlobalConditionType + * @phpstan-type RequestConditionType IContextualConditionType * @extends AbstractConditionProvider */ final class RequestConditionProvider extends AbstractConditionProvider diff --git a/wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IContextualConditionType.class.php similarity index 88% rename from wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/IContextualConditionType.class.php index ee7e3cd28e3..33df27795a8 100644 --- a/wcfsetup/install/files/lib/system/condition/type/IGlobalConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/IContextualConditionType.class.php @@ -11,7 +11,7 @@ * @template TFilter * @extends IConditionType */ -interface IGlobalConditionType extends IConditionType +interface IContextualConditionType extends IConditionType { /** * Returns `true` if the condition matches the global context (e.g., the active user via `WCF::getUser()`). 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 cd7f6072180..aba75d59704 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 @@ -4,7 +4,7 @@ use wcf\data\page\PageNodeTree; use wcf\system\condition\type\AbstractConditionType; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\request\RequestHandler; @@ -15,10 +15,10 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IGlobalConditionType + * @implements IContextualConditionType * @extends AbstractConditionType */ -final class ActivePageRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType +final class ActivePageRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string 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 e8d477ba64e..fff85504659 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 @@ -3,7 +3,7 @@ namespace wcf\system\condition\type\request; use wcf\system\condition\type\AbstractConditionType; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\WCF; @@ -15,10 +15,10 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IGlobalConditionType + * @implements IContextualConditionType * @extends AbstractConditionType */ -final class DayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType +final class DayOfWeekRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string 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 50fddc3b25d..f6853a99a60 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 @@ -3,7 +3,7 @@ namespace wcf\system\condition\type\request; use wcf\system\condition\type\AbstractConditionType; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\WCF; @@ -15,10 +15,10 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IGlobalConditionType + * @implements IContextualConditionType * @extends AbstractConditionType */ -final class NotDayOfWeekRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType +final class NotDayOfWeekRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string 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 1b72f95958d..4298f78e84f 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 @@ -4,7 +4,7 @@ use wcf\data\page\PageNodeTree; use wcf\system\condition\type\AbstractConditionType; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\request\RequestHandler; @@ -16,10 +16,10 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IGlobalConditionType + * @implements IContextualConditionType * @extends AbstractConditionType */ -final class NotOnPageRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType +final class NotOnPageRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType { #[\Override] public function getIdentifier(): string 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 914a954a526..2784c364ead 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 @@ -3,7 +3,7 @@ namespace wcf\system\condition\type\request; use wcf\system\condition\type\AbstractConditionType; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\form\builder\container\condition\RowConditionFormFieldContainer; use wcf\system\form\builder\field\SingleSelectionFormField; @@ -18,10 +18,10 @@ * @since 6.3 * * @phpstan-type Filter = array{Condition: string, Value: string, Timezone: string} - * @implements IGlobalConditionType + * @implements IContextualConditionType * @extends AbstractConditionType */ -final class TimeRequestConditionType extends AbstractConditionType implements IGlobalConditionType, IMigrateConditionType +final class TimeRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType { public const USER_TIMEZONE = 'userTimezone'; diff --git a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php index 56d966cc5b6..fea394da139 100644 --- a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php +++ b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php @@ -6,7 +6,7 @@ use wcf\system\cache\eager\NoticeCache; use wcf\system\condition\ConditionHandler; use wcf\system\condition\provider\combined\NoticeConditionProvider; -use wcf\system\condition\type\IGlobalConditionType; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\SingletonFactory; use wcf\system\WCF; @@ -59,7 +59,7 @@ public function getVisibleNotices() $conditions = ConditionHandler::getInstance()->getConditionsWithFilter($provider, $notice->getConditions()); foreach ($conditions as $condition) { - $matches = $condition instanceof IGlobalConditionType + $matches = $condition instanceof IContextualConditionType ? $condition->matches() : $condition->matches(WCF::getUser()); From 2a7fcf7d0d4722fb9083c294bcd15e5f3c677781 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 16 Jul 2025 13:15:26 +0200 Subject: [PATCH 11/14] Remove unnecessary `CombinedConditionProvider`. Implement the function `withConditionsFrom()` to transfer conditions from another provider. --- .../AbstractConditionProvider.class.php | 9 +++++ .../CombinedConditionProvider.class.php | 33 ------------------- .../NoticeConditionProvider.class.php | 10 +++--- 3 files changed, 13 insertions(+), 39 deletions(-) delete mode 100644 wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php diff --git a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php index f3ee4f3879d..0280b6823b2 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php @@ -78,4 +78,13 @@ public function getConditionTypes(): array { return $this->conditionTypes; } + + public function withConditionsFrom(AbstractConditionProvider $provider): self + { + foreach ($provider->getConditionTypes() as $conditionType) { + $this->addCondition($conditionType); + } + + return $this; + } } diff --git a/wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php deleted file mode 100644 index c0174240927..00000000000 --- a/wcfsetup/install/files/lib/system/condition/provider/combined/CombinedConditionProvider.class.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 6.3 - * - * @template TConditionType of IConditionType - * @template TConditionProvider of AbstractConditionProvider - * @extends AbstractConditionProvider - */ -abstract class CombinedConditionProvider extends AbstractConditionProvider -{ - /** - * @param TConditionProvider ...$providers - */ - public function __construct(AbstractConditionProvider ...$providers) - { - foreach ($providers as $provider) { - foreach ($provider->getConditionTypes() as $condition) { - $this->addCondition($condition); - } - } - } -} diff --git a/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php index 44de926890f..d304f967647 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php @@ -14,15 +14,13 @@ * * @phpstan-import-type RequestConditionType from RequestConditionProvider * @phpstan-import-type UserConditionType from UserConditionProvider - * @extends CombinedConditionProvider|AbstractConditionProvider> + * @extends AbstractConditionProvider */ -final class NoticeConditionProvider extends CombinedConditionProvider +final class NoticeConditionProvider extends AbstractConditionProvider { public function __construct() { - parent::__construct( - new UserConditionProvider(), - new RequestConditionProvider(), - ); + $this->withConditionsFrom(new UserConditionProvider()); + $this->withConditionsFrom(new RequestConditionProvider()); } } From 9e8a1d562437ee26a21f502b08ac5ac6eeb2b75b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 17 Jul 2025 10:21:31 +0200 Subject: [PATCH 12/14] =?UTF-8?q?The=20active=20page=20must=20be=20a=20mul?= =?UTF-8?q?tiple=20selection=20so=20that=20an=20=E2=80=9Cor=E2=80=9D=20con?= =?UTF-8?q?dition=20is=20possible.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActivePageRequestConditionType.class.php | 28 ++++++++----------- .../NotOnPageRequestConditionType.class.php | 8 +----- .../ConditionFormContainer.class.php | 4 +-- 3 files changed, 15 insertions(+), 25 deletions(-) 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 aba75d59704..af5436701ce 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 @@ -6,7 +6,7 @@ use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IContextualConditionType; use wcf\system\condition\type\IMigrateConditionType; -use wcf\system\form\builder\field\SingleSelectionFormField; +use wcf\system\form\builder\field\MultipleSelectionFormField; use wcf\system\request\RequestHandler; /** @@ -15,8 +15,8 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IContextualConditionType - * @extends AbstractConditionType + * @implements IContextualConditionType + * @extends AbstractConditionType */ final class ActivePageRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType { @@ -33,19 +33,18 @@ public function getLabel(): string } #[\Override] - public function getFormField(string $id): SingleSelectionFormField + public function getFormField(string $id): MultipleSelectionFormField { - // SelectFormField stores its value as a string, - // so we need to convert it to an integer in the `matches` method. - return SingleSelectionFormField::create($id) + return MultipleSelectionFormField::create($id) ->options((new PageNodeTree())->getNodeList(), true) + ->filterable() ->required(); } #[\Override] public function matches(): bool { - return RequestHandler::getInstance()->getActivePageID() === (int)$this->filter; + return \in_array(RequestHandler::getInstance()->getActivePageID(), $this->filter); } #[\Override] @@ -54,18 +53,15 @@ public function migrateConditionData(array &$conditionData): array $reverseLogic = $conditionData['pageIDs_reverseLogic'] ?? false; $pageIDs = $conditionData['pageIDs'] ?? []; - if ($reverseLogic || \count($pageIDs) > 1) { + if ($reverseLogic) { // `NotOnPageRequestConditionType` should migrate the data. return []; } - $conditions = []; - foreach ($pageIDs as $pageID) { - $conditions[] = [ - 'identifier' => $this->getIdentifier(), - 'value' => (string)$pageID, - ]; - } + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => \array_map('strval', $pageIDs), + ]; unset($conditionData['pageIDs'], $conditionData['pageIDs_reverseLogic']); 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 4298f78e84f..3b80a49c737 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 @@ -55,18 +55,12 @@ public function migrateConditionData(array &$conditionData): array $reverseLogic = $conditionData['pageIDs_reverseLogic'] ?? false; $pageIDs = $conditionData['pageIDs'] ?? []; - if (!$reverseLogic && \count($pageIDs) <= 1) { + if (!$reverseLogic) { // `ActivePageRequestConditionType` should migrate the data. return []; } $conditions = []; - if (!$reverseLogic) { - // If reverse logic is not activated, we must add all unselected pages. - // This allows us to turn an “or” condition into an “and” condition. - $pageIDs = \array_diff($this->getPageIDs(), $pageIDs); - } - foreach ($pageIDs as $pageID) { $conditions[] = [ 'identifier' => $this->getIdentifier(), diff --git a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index 7a2ef60c2ed..9256854bf1f 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -112,10 +112,10 @@ private function appendCondition(string $identifier, int $index): FormContainer static function (IFormDocument $document, array $parameters) use ($prefixId, $identifier, $fieldId) { $conditions = isset($parameters['data'][$prefixId]) ? JSON::decode($parameters['data'][$prefixId]) : []; - if (isset($parameters['data'][$fieldId])) { + if (isset($parameters['data'][$fieldId]) || isset($parameters[$fieldId])) { $conditions[] = [ "identifier" => $identifier, - "value" => $parameters['data'][$fieldId], + "value" => $parameters['data'][$fieldId] ?? $parameters[$fieldId], ]; } From 3013d20abea2c71c3a58773dd39ca4fb70ff41ad Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 18 Jul 2025 12:51:50 +0200 Subject: [PATCH 13/14] Add the information to the ACP Status Message box that the Rebuild Data must be performed for notes. --- .../box/StatusMessageAcpDashboardBox.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php b/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php index c70653a43b8..e5083a6a3ab 100644 --- a/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php +++ b/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php @@ -283,9 +283,13 @@ private function getMigrationMessage(): array { $event = new MigrationCollecting(); EventHandler::getInstance()->fire($event); + if ($this->userGroupAssignmentHasLegacyObjects()) { $event->migrationNeeded(WCF::getLanguage()->get('wcf.acp.group.assignment')); } + if ($this->noticeHasLegacyObjects()) { + $event->migrationNeeded(WCF::getLanguage()->get('wcf.acp.notice.list')); + } if ($event->needsMigration() === []) { return []; @@ -311,4 +315,15 @@ private function userGroupAssignmentHasLegacyObjects(): bool return $statement->fetchColumn() > 0; } + + private function noticeHasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_notice + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } From 9f03cace79e5c2a557ec44433d434628f7256ed7 Mon Sep 17 00:00:00 2001 From: Olaf Braun Date: Wed, 23 Jul 2025 08:31:45 +0200 Subject: [PATCH 14/14] Grouped condition types (#6387) * Group the condition types and make it possible to open/close the groups in the input field. * Minor code fixes * Remove the collapse feature, improve visuals of categories * Simplify the filtering, use native `hidden` property instead --------- Co-authored-by: Alexander Ebert --- ...ed_categorizedSingleSelectionFormField.tpl | 43 ++++++ .../Core/Component/ItemList/Categorized.ts | 140 ++++++++++++++++++ .../Core/Component/ItemList/Categorized.js | 112 ++++++++++++++ .../lib/action/ConditionAddAction.class.php | 81 ++++++++-- .../provider/UserConditionProvider.class.php | 10 ++ .../condition/type/IConditionType.class.php | 7 + .../ActivePageRequestConditionType.class.php | 6 + .../DayOfWeekRequestConditionType.class.php | 6 + ...NotDayOfWeekRequestConditionType.class.php | 6 + .../NotOnPageRequestConditionType.class.php | 6 + .../TimeRequestConditionType.class.php | 6 + .../user/BooleanUserConditionType.class.php | 7 + .../HasNotTrophyUserConditionType.class.php | 6 + .../user/HasTrophyUserConditionType.class.php | 6 + .../user/InGroupUserConditionType.class.php | 6 + .../user/IntegerUserConditionType.class.php | 7 + .../user/IsEnabledConditionType.class.php | 2 +- .../user/IsNullUserConditionType.class.php | 7 + .../user/LanguageUserConditionType.class.php | 6 + .../NotInGroupUserConditionType.class.php | 6 + ...egistrationDateUserConditionType.class.php | 6 + ...egistrationDaysUserConditionType.class.php | 6 + .../user/SignatureUserConditionType.class.php | 1 + .../user/StringUserConditionType.class.php | 7 + .../style/ui/scrollableCheckboxList.scss | 22 +++ wcfsetup/install/lang/de.xml | 4 + wcfsetup/install/lang/en.xml | 4 + 27 files changed, 514 insertions(+), 12 deletions(-) create mode 100644 com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl create mode 100644 ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js diff --git a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl new file mode 100644 index 00000000000..4e204d2fb51 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl @@ -0,0 +1,43 @@ + + +
+
+ + +
+
    + {foreach from=$field->getNestedOptions() item=__fieldNestedOption} +
  • 0} style="padding-left: {$__fieldNestedOption[depth]*20}px"{/if} + {if !$__fieldNestedOption[isSelectable]} class="scrollableCheckboxList__category"{/if} + > + {if !$__fieldNestedOption[isSelectable]} + {unsafe:$__fieldNestedOption[label]} + {else} + + {/if} +
  • + {/foreach} +
+
diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts new file mode 100644 index 00000000000..f0eecbafa1c --- /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 00000000000..92bd82ae7d5 --- /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 83c4c2772b7..193571b4347 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 6ac0a8792bc..b1d3736336b 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 03b9aa4219a..fee6e9f8c03 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 af5436701ce..b1e075568a7 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 fff85504659..ef6b2a5f2be 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 f6853a99a60..c415c74f51c 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 3b80a49c737..1b29d932a16 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 2784c364ead..06aa7371f3e 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 c2ee9504da4..561a9de226c 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 2c2306ae1b7..523f2320458 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 72b710fb7d5..8333af33177 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 3b57a266753..a64510d45bf 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 5b427ff3a4a..9319ece74e0 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 1a81c66eb14..84cdcd27b2f 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 6f243bc89ff..f032230ec9d 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 ba74a10afd4..04ae4fa1d39 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 12731c3864e..126edf1b97a 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 717d5b0fcc9..51ecca460a6 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 febacbd506a..dd26a01eec6 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 30e5e74b8ad..f8b8fffeacd 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 d3f6d022b1e..6032f4eb5b8 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 2ef8fb14209..02feb3d11de 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 f9d817fdfb7..3fb5b6c8e0f 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 bd2e8c60d78..3be6ec4e537 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3493,6 +3493,10 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi + + + +