diff --git a/XSD/userOption.xsd b/XSD/userOption.xsd index 4e805d33ff3..581b398ff64 100644 --- a/XSD/userOption.xsd +++ b/XSD/userOption.xsd @@ -41,6 +41,8 @@ + + @@ -66,11 +68,12 @@ + - + @@ -90,6 +93,9 @@ + + + diff --git a/com.woltlab.wcf/acpTemplateDelete.xml b/com.woltlab.wcf/acpTemplateDelete.xml index d147759f63b..9bf79c79679 100644 --- a/com.woltlab.wcf/acpTemplateDelete.xml +++ b/com.woltlab.wcf/acpTemplateDelete.xml @@ -157,5 +157,11 @@ + + + + + + diff --git a/com.woltlab.wcf/clipboardAction.xml b/com.woltlab.wcf/clipboardAction.xml index 66c6f2ebf5e..21ffa86a7bf 100644 --- a/com.woltlab.wcf/clipboardAction.xml +++ b/com.woltlab.wcf/clipboardAction.xml @@ -1,6 +1,35 @@ + + + wcf\system\clipboard\action\MediaClipboardAction + 1 + + menuManagerDialog-editor + + + + wcf\system\clipboard\action\MediaClipboardAction + 2 + + wcf\acp\page\MediaListPage + menuManagerDialog-editor + menuManagerDialog-select + + + + wcf\system\clipboard\action\MediaClipboardAction + 3 + + wcf\acp\page\MediaListPage + menuManagerDialog-editor + menuManagerDialog-select + + + + + wcf\system\clipboard\action\UserClipboardAction @@ -94,35 +123,6 @@ - - - wcf\system\clipboard\action\MediaClipboardAction - 1 - - menuManagerDialog-editor - - - - wcf\system\clipboard\action\MediaClipboardAction - 2 - - wcf\acp\page\MediaListPage - menuManagerDialog-editor - menuManagerDialog-select - - - - wcf\system\clipboard\action\MediaClipboardAction - 3 - - wcf\acp\page\MediaListPage - menuManagerDialog-editor - menuManagerDialog-select - - - - - wcf\system\clipboard\action\ArticleClipboardAction diff --git a/com.woltlab.wcf/fileDelete.xml b/com.woltlab.wcf/fileDelete.xml index b75e6e2cbe4..0059cf6e82a 100644 --- a/com.woltlab.wcf/fileDelete.xml +++ b/com.woltlab.wcf/fileDelete.xml @@ -8,6 +8,30 @@ js/WoltLabSuite/Core/Ui/User/CoverPhoto/Delete.js js/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.js lib/form/AvatarEditForm.class.php + lib/form/UserSearchForm.class.php + lib/action/EmailValidationAction.class.php + lib/action/UsernameValidationAction.class.php + js/WoltLabSuite/Core/Acp/Controller/User/List.js + js/WoltLabSuite/Core/Acp/Ui/User/Editor.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/Abstract.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/BanAction.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/DeleteAction.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/DisableAction.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/SendNewPasswordAction.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/ToggleConfirmEmailAction.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban/Dialog.js + js/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Delete.js + js/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Clipboard.js + js/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Handler.js + js/WoltLabSuite/Core/Ui/User/Editor.js + lib/acp/action/UserQuickSearchAction.class.php + lib/acp/form/UserSearchForm.class.php + lib/data/user/UserContentAction.class.php + lib/data/user/UserRegistrationAction.class.php + lib/system/clipboard/action/UserClipboardAction.class.php + lib/system/clipboard/action/UserContentClipboardAction.class.php + lib/system/listView/user/UserSearchResultsListView.class.php lib/system/api/cuyz/valinor/qa/PHPStan/Stubs/Psr/SimpleCache/CacheInterface.stub lib/system/api/cuyz/valinor/src/Cache/ChainCache.php lib/system/api/cuyz/valinor/src/Cache/WarmupCache.php @@ -76,6 +100,23 @@ lib/system/upload/AvatarUploadFileValidationStrategy.class.php lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php lib/system/upload/UserCoverPhotoUploadFileValidationStrategy.class.php + lib/system/option/user/BirthdayUserOptionOutput.class.php + lib/system/option/user/DateUserOptionOutput.class.php + lib/system/option/user/FacebookUserOptionOutput.class.php + lib/system/option/user/FloatUserOptionOutput.class.php + lib/system/option/user/ImageUserOptionOutput.class.php + lib/system/option/user/LabeledUrlUserOptionOutput.class.php + lib/system/option/user/MessageUserOptionOutput.class.php + lib/system/option/user/NewlineToBreakUserOptionOutput.class.php + lib/system/option/user/SelectOptionsUserOptionOutput.class.php + lib/system/option/user/TwitterUserOptionOutput.class.php + lib/system/option/user/URLUserOptionOutput.class.php + lib/system/option/user/UserOptionHandler.class.php + lib/acp/form/UserOptionListForm.class.php + lib/system/condition/UserOptionsCondition.class.php + lib/acp/form/UserContentRevertChangesForm.class.php + lib/acp/form/UserAssignToGroupForm.class.php + lib/acp/form/UserEmailAddressExportForm.class.php icon/font-awesome/v6/brands/42-group.svg icon/font-awesome/v6/brands/500px.svg diff --git a/com.woltlab.wcf/menuItem.xml b/com.woltlab.wcf/menuItem.xml index 10d767af59e..438cdf36f49 100644 --- a/com.woltlab.wcf/menuItem.xml +++ b/com.woltlab.wcf/menuItem.xml @@ -49,12 +49,6 @@ Staff com.woltlab.wcf.Team - - com.woltlab.wcf.MembersList - Mitgliedersuche - Search Members - com.woltlab.wcf.UserSearch - com.woltlab.wcf.FooterMenu Datenschutzerklärung diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index 6ee56891aaa..c384a1714a8 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -2,26 +2,11 @@ - - com.woltlab.wcf.user - com.woltlab.wcf.clipboardItem - wcf\data\user\UserList - - - com.woltlab.wcf.tag - com.woltlab.wcf.clipboardItem - wcf\data\tag\TagList - com.woltlab.wcf.media com.woltlab.wcf.clipboardItem wcf\data\media\ViewableMediaList - - com.woltlab.wcf.article - com.woltlab.wcf.clipboardItem - wcf\data\article\ArticleList - @@ -667,12 +652,6 @@ likesReceived 0 - - com.woltlab.wcf.userOptions - com.woltlab.wcf.condition.trophy - wcf\system\condition\UserOptionsCondition - userOptions - com.woltlab.wcf.userTrophyCondition com.woltlab.wcf.condition.trophy @@ -957,12 +936,6 @@ likesReceived 0 - - com.woltlab.wcf.userOptions - com.woltlab.wcf.bulkProcessing.user.condition - wcf\system\condition\UserOptionsCondition - userOptions - com.woltlab.wcf.userTrophyCondition com.woltlab.wcf.bulkProcessing.user.condition @@ -978,109 +951,6 @@ 0 - - - com.woltlab.wcf.userID - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserUserIDCondition - general - - - com.woltlab.wcf.username - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserUsernameCondition - general - - - com.woltlab.wcf.email - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserEmailCondition - general - - - com.woltlab.wcf.userGroup - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserGroupCondition - general - - - com.woltlab.wcf.languages - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserLanguageCondition - general - - - com.woltlab.wcf.registrationDate - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserRegistrationDateCondition - general - - - com.woltlab.wcf.registrationDateInterval - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserRegistrationDateIntervalCondition - general - - - com.woltlab.wcf.lastActivityTime - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserTimestampPropertyCondition - general - lastActivityTime - - - com.woltlab.wcf.avatar - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserAvatarCondition - general - - - com.woltlab.wcf.signature - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserSignatureCondition - general - - - com.woltlab.wcf.coverPhoto - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserCoverPhotoCondition - general - - - com.woltlab.wcf.state - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserStateCondition - general - - - com.woltlab.wcf.activityPoints - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserIntegerPropertyCondition - contents - activityPoints - 0 - - - com.woltlab.wcf.likesReceived - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserIntegerPropertyCondition - contents - likesReceived - 0 - - - com.woltlab.wcf.userOptions - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserOptionsCondition - userOptions - - - com.woltlab.wcf.user.multifactor - com.woltlab.wcf.condition.userSearch - wcf\system\condition\UserMultifactorCondition - general - - com.woltlab.wcf.recentActivityList @@ -1613,5 +1483,70 @@ com.woltlab.wcf.condition.ad + + com.woltlab.wcf.clipboardItem + + + com.woltlab.wcf.clipboardItem + + + com.woltlab.wcf.clipboardItem + + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + com.woltlab.wcf.condition.userSearch + + + + com.woltlab.wcf.condition.trophy + + + com.woltlab.wcf.bulkProcessing.user.condition + diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index a9348bec6b4..190e29d1ff2 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -131,6 +131,7 @@ com.woltlab.wcf.condition.notice wcf\system\condition\IContentCondition + com.woltlab.wcf.condition.userSearch wcf\system\condition\IObjectListCondition diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index d4f63a11a66..f6e92e09037 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -87,12 +87,19 @@ acp/update_com.woltlab.wcf_6.3_contactRecipientL10n.php acp/update_com.woltlab.wcf_6.3_paidSubscriptionL10n.php acp/update_com.woltlab.wcf_6.3_userOptionL10n.php + acp/update_com.woltlab.wcf_6.3_userOptionCategoryL10n.php acp/update_com.woltlab.wcf_6.3_captchaQuestionL10n.php + acp/update_com.woltlab.wcf_6.3_userOptionConfiguration.php acp/database/update_com.woltlab.wcf_6.3_step3.php The following step requires the new files and must run before the `cronjob` PIP, which precedes the `objectType` PIP: acp/update_com.woltlab.wcf_6.3_cronjobL10n.php `acp/database/update_com.woltlab.wcf_6.3_step3.php` must run after all of the scripts above, including `acp/update_com.woltlab.wcf_6.3_cronjobL10n.php`. + + The `userOption` PIP must run after `update_com.woltlab.wcf_6.3_userOptionConfiguration.php`, + because the script rebuilds the configuration of all user options and would discard the + social media profile configuration of the `facebook` and `twitter` options. + --> diff --git a/com.woltlab.wcf/page.xml b/com.woltlab.wcf/page.xml index c3acbad30a7..47d092f9306 100644 --- a/com.woltlab.wcf/page.xml +++ b/com.woltlab.wcf/page.xml @@ -59,21 +59,6 @@ Team - - system - wcf\form\UserSearchForm - Benutzer suchen - Search Users - com.woltlab.wcf.MembersList - module_members_list - user.profile.canViewMembersList - - Search Users - - - Benutzer suchen - - system wcf\form\AccountManagementForm @@ -915,5 +900,7 @@ E-Mail: [E-Mail-Adresse der verantwortlichen Stelle]

Verantwortliche Stell + + diff --git a/com.woltlab.wcf/templateDelete.xml b/com.woltlab.wcf/templateDelete.xml index 7198bdb3fa6..9a3ab2a01d7 100644 --- a/com.woltlab.wcf/templateDelete.xml +++ b/com.woltlab.wcf/templateDelete.xml @@ -118,5 +118,16 @@ + + + + + + + + + + + diff --git a/com.woltlab.wcf/templates/__devtoolsLanguageChooser.tpl b/com.woltlab.wcf/templates/__devtoolsLanguageChooser.tpl index 9183c08f0ed..baa2c105c09 100644 --- a/com.woltlab.wcf/templates/__devtoolsLanguageChooser.tpl +++ b/com.woltlab.wcf/templates/__devtoolsLanguageChooser.tpl @@ -1,7 +1,7 @@ {* The `system_pageHeader` renders this chooser on the server. *} {if ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS && !$__wcf->user->isGuest() && $__wcf->getStyleHandler()->getStyle()->usesClassicPageHeader()} diff --git a/com.woltlab.wcf/templates/membersList.tpl b/com.woltlab.wcf/templates/membersList.tpl index 9d28d5f31da..ba7e8609200 100644 --- a/com.woltlab.wcf/templates/membersList.tpl +++ b/com.woltlab.wcf/templates/membersList.tpl @@ -1,12 +1,3 @@ -{if $searchID} - {capture assign='pageTitle'}{lang}wcf.user.search.results{/lang}{/capture} - {capture assign='contentTitle'}{lang}wcf.user.search.results{/lang}{/capture} -{/if} - -{capture assign='contentInteractionButtons'} - {icon name='search'} {lang}wcf.user.search{/lang} -{/capture} - {include file='header'}

diff --git a/com.woltlab.wcf/templates/messageUserOptionOutput.tpl b/com.woltlab.wcf/templates/messageUserOptionOutput.tpl deleted file mode 100644 index d6da2f16af6..00000000000 --- a/com.woltlab.wcf/templates/messageUserOptionOutput.tpl +++ /dev/null @@ -1,11 +0,0 @@ -{hascontent} -
- {content} - {unsafe:$value} - {/content} -
- - -{/hascontent} diff --git a/com.woltlab.wcf/templates/multiSelectSearchableOptionType.tpl b/com.woltlab.wcf/templates/multiSelectSearchableOptionType.tpl deleted file mode 100644 index e4d9b4b307f..00000000000 --- a/com.woltlab.wcf/templates/multiSelectSearchableOptionType.tpl +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/com.woltlab.wcf/templates/register.tpl b/com.woltlab.wcf/templates/register.tpl index e6831cc5d1e..3c7f791a31c 100644 --- a/com.woltlab.wcf/templates/register.tpl +++ b/com.woltlab.wcf/templates/register.tpl @@ -6,236 +6,29 @@ {lang}wcf.user.3rdparty.{$__wcf->session->getVar('__3rdPartyProvider')}.register{/lang} {/if} -{include file='shared_formError'} - -
- - -
- -
- * -
-
- - {if $errorType[username]|isset} - - {if $errorType[username] == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.username.error.{$errorType[username]}{/lang} - {/if} - - {/if} - {lang}wcf.user.username.description{/lang} -
- - - -
- * -
-
- - {if $errorType[email]|isset} - - {if $errorType[email] == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.email.error.{$errorType[email]}{/lang} - {/if} - - {/if} -
- - - {if !$isExternalAuthentication} -
-
- * -
-
- - {if $errorType[password]|isset} - - {if $errorType[password] == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.password.error.{$errorType[password]}{/lang} - {/if} - - {/if} - {lang}wcf.user.password.description{/lang} -
-
- {/if} - - {if $availableLanguages|count > 1} -
-
-
- - -
-
- - {hascontent} -
-
-
- {content} - {foreach from=$availableContentLanguages item=language} - - {/foreach} - {/content} - {lang}wcf.user.visibleLanguages.description{/lang}
-
- {/hascontent} - {/if} - - {if REGISTER_ENABLE_DISCLAIMER} - -
-
- - {if $errorType[termsConfirmed]|isset} - - {lang}wcf.global.form.error.empty{/lang} - - {/if} -
- - {/if} - - {event name='generalFields'} -
- - {foreach from=$optionTree item=category} - {include file='userOptionFieldList' options=$category[options] langPrefix='wcf.user.option.'} - {/foreach} - - {event name='sections'} - - {include file='shared_captcha'} - -
- - {csrfToken} -
- - {include file='thirdPartySsoButtons'} -
- -

- * - {lang}wcf.global.form.required{/lang} -

- - + +{/if} {include file='authFlowFooter'} diff --git a/com.woltlab.wcf/templates/selectSearchableOptionType.tpl b/com.woltlab.wcf/templates/selectSearchableOptionType.tpl deleted file mode 100644 index e05649bc5e5..00000000000 --- a/com.woltlab.wcf/templates/selectSearchableOptionType.tpl +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/com.woltlab.wcf/templates/settings.tpl b/com.woltlab.wcf/templates/settings.tpl index ccb855d79bb..e63bd0d4a98 100644 --- a/com.woltlab.wcf/templates/settings.tpl +++ b/com.woltlab.wcf/templates/settings.tpl @@ -6,143 +6,6 @@ {include file='header' __disableAds=true __sidebarLeftHasMenu=true} -{include file='shared_formError'} - -{if $success|isset} - {lang}wcf.global.success.edit{/lang} -{/if} - -
- {if $category == 'general'} - {if $availableLanguages|count > 1} -
-

{lang}wcf.user.language{/lang}

- -
-
-
- - -
-
- - {hascontent} -
-
-
- {content} - {foreach from=$availableContentLanguages item=language} - - {/foreach} - {/content} - {lang}wcf.user.visibleLanguages.description{/lang}
-
- {/hascontent} - - {event name='languageFields'} -
- {/if} - -
-

{lang}wcf.user.styles{/lang}

- - {if $availableStyles|count > 1} -
-
-
- - {lang}wcf.user.style.description{/lang} -
-
- {/if} - -
-
-
- - {lang}wcf.user.style.colorScheme.description{/lang} -
-
- - {event name='styleFields'} -
- - {if MODULE_TROPHY && $__wcf->getSession()->getPermission('user.profile.trophy.maxUserSpecialTrophies') > 0 && $availableTrophies|count} -
-

{lang}wcf.user.trophy.trophies{/lang}

- -
{lang}wcf.user.trophy.specialTrophies{/lang}
-
-
    - {if $__wcf->getSession()->getPermission('user.profile.trophy.maxUserSpecialTrophies') == 1} - {foreach from=$availableTrophies item=trophy} -
  • - {/foreach} - {else} - {foreach from=$availableTrophies item=trophy} -
  • - {/foreach} - {/if} -
- {if $errorField == 'specialTrophies'} - - {lang}wcf.user.trophy.specialTrophies.error.{$errorType}{/lang} - - {/if} - {lang}wcf.user.trophy.specialTrophies.description{/lang} -
- - - {event name='trophyFields'} -
- {/if} - {/if} - - {if !$optionTree|empty} - {foreach from=$optionTree[0][categories][0][categories] item=optionCategory} -
-

{lang}wcf.user.option.category.{$optionCategory[object]->categoryName}{/lang}

- - {include file='userProfileOptionFieldList' options=$optionCategory[options] langPrefix='wcf.user.option.'} -
- {/foreach} - {/if} - - {event name='sections'} - -
- - {if $category != 'general'}{/if} - {csrfToken} -
-
+{unsafe:$form->getHtml()} {include file='footer' __disableAds=true} diff --git a/com.woltlab.wcf/templates/shared_adminUserProfileHeader.tpl b/com.woltlab.wcf/templates/shared_adminUserProfileHeader.tpl new file mode 100644 index 00000000000..29d29761c12 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_adminUserProfileHeader.tpl @@ -0,0 +1,155 @@ +
+
+
+ +
+ +
+ {event name='beforeManageButtons'} + + {if $view->canEditCoverPhoto()} + + {/if} + + {if $view->canEditAvatar()} + + {/if} + + {event name='afterManageButtons'} +
+
+ +
+
+
+ {unsafe:$view->getAvatarImageTag()} + + {if $view->user->isOnline()}{/if} +
+
+
+
+

+ {$view->user->username} +

+ +
+ {if $view->user->banned} + + {icon name='lock'} + + {/if} + + {event name='icons'} +
+
+ + {event name='afterUsername'} + +
+ {if MODULE_USER_RANK} + {if $view->user->getUserTitle()} + {$view->user->getUserTitle()} + {/if} + {if $view->user->getRank() && $view->user->getRank()->rankImage} + {unsafe:$view->user->getRank()->getImage()} + {/if} + {/if} +
+ {event name='afterUserTitle'} +
+
+ {foreach from=$view->getStatItems() item='statItem'} + {if $statItem->link} + attributes}> + {unsafe:$statItem->title} + {unsafe:$statItem->value} + + {else} + {* Buttons rely on frontend-only JavaScript and are therefore shown as plain text. *} +
+ {unsafe:$statItem->title} + {unsafe:$statItem->value} +
+ {/if} + {/foreach} +
+
+ {event name='beforeButtons'} + + {hascontent} + {content}{unsafe:$view->getInteractionContextMenu()->render()}{/content} + + + {/hascontent} + + {if $view->hasSearchContentLinks()} + + {/if} + + {event name='afterButtons'} +
+ +
+
+ {event name='beforeMeta'} + +
{icon name='calendar'} {lang}wcf.user.registrationDate{/lang}
+
{time time=$view->user->registrationDate type='plainDate'}
+ {if $view->user->getOldUsername()} +
{icon name='scroll'} {lang}wcf.user.oldUsername{/lang}
+
{$view->user->getOldUsername()}
+ {/if} + {if $view->user->canViewOnlineStatus() && $view->user->getLastActivityTime()} +
{icon name='clock'} {lang}wcf.user.usersOnline.lastActivity{/lang}
+
{time time=$view->user->getLastActivityTime()}
+ {if $view->user->getCurrentLocation()} +
{icon name='location-arrow'} {lang}wcf.user.usersOnline.location{/lang}
+
{unsafe:$view->user->getCurrentLocation()}
+ {/if} + {/if} + {if $__wcf->session->getPermission('admin.user.canViewIpAddress') && $view->user->registrationIpAddress} +
{icon name='network-wired'} {lang}wcf.user.registrationIpAddress{/lang}
+
{unsafe:$view->user->getRegistrationIpAddress()|ipSearch}
+ {/if} + {if $__wcf->session->getPermission('admin.user.canEditPassword')} + {if !$view->user->authData|empty} +
{icon name='arrow-right-to-bracket'} {lang}wcf.user.3rdparty{/lang}
+
{lang}wcf.user.3rdparty.{$view->user->getAuthProvider()}{/lang}
+ {/if} + {if $view->user->multifactorActive} +
{icon name='fingerprint'} {lang}wcf.user.security.multifactor{/lang}
+
{lang}wcf.user.security.multifactor.active{/lang}
+ {/if} + {/if} + + {event name='afterMeta'} +
+
+
+
+ + diff --git a/com.woltlab.wcf/templates/shared_gridView.tpl b/com.woltlab.wcf/templates/shared_gridView.tpl index b44f4a2161c..381d1e33f05 100644 --- a/com.woltlab.wcf/templates/shared_gridView.tpl +++ b/com.woltlab.wcf/templates/shared_gridView.tpl @@ -1,5 +1,5 @@
- {if $view->isFilterable()} + {if $view->isFilterable() || $view->isColumnConfigurable()}
{foreach from=$view->getActiveFilters() item='value' key='key'} @@ -16,10 +16,18 @@ {/foreach}
- + {if $view->isColumnConfigurable()} + + {/if} + {if $view->isFilterable()} + + {/if}
{/if} @@ -33,29 +41,7 @@ {/if} - {foreach from=$view->getVisibleColumns() item='column'} - isSortable()}data-default-sort-order="{$column->getDefaultSortOrder()}"{/if} - {if $column->isSortable()} - {if $view->isSortedBy($column)} - aria-sort="{if $view->getSortOrder() === 'ASC'}ascending{else}descending{/if}" - {else} - aria-sort="none" - {/if} - {/if} - > - {if $column->isSortable()} - - {else} - {unsafe:$column->getLabel()} - {/if} - - {/foreach} + {unsafe:$view->renderHeaderColumns()} {if $view->hasInteractions()} {/if} @@ -117,6 +103,7 @@ ['{unsafe:$name|encodeJS}', {unsafe:$value|json}], {/foreach} ]), + {if $view->getVisibleColumnIDs() !== null}{unsafe:$view->getVisibleColumnIDs()|json}{else}undefined{/if}, ); }); diff --git a/com.woltlab.wcf/templates/shared_gridViewHeaderColumns.tpl b/com.woltlab.wcf/templates/shared_gridViewHeaderColumns.tpl new file mode 100644 index 00000000000..6f3972ce465 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_gridViewHeaderColumns.tpl @@ -0,0 +1,23 @@ +{foreach from=$view->getVisibleColumns() item='column'} + isSortable()}data-default-sort-order="{$column->getDefaultSortOrder()}"{/if} + {if $column->isSortable()} + {if $view->isSortedBy($column)} + aria-sort="{if $view->getSortOrder() === 'ASC'}ascending{else}descending{/if}" + {else} + aria-sort="none" + {/if} + {/if} + > + {if $column->isSortable()} + + {else} + {unsafe:$column->getLabel()} + {/if} + +{/foreach} diff --git a/com.woltlab.wcf/templates/shared_userConditions.tpl b/com.woltlab.wcf/templates/shared_userConditions.tpl index 56077ea7471..94d1f5382c4 100644 --- a/com.woltlab.wcf/templates/shared_userConditions.tpl +++ b/com.woltlab.wcf/templates/shared_userConditions.tpl @@ -11,18 +11,13 @@ {foreach from=$groupedObjectTypes key='conditionGroup' item='conditionObjectTypes'}
- {if $conditionGroup != 'userOptions'} -
-

{lang}wcf.user.condition.conditionGroup.{$conditionGroup}{/lang}

- {/if} - - {foreach from=$conditionObjectTypes item='condition'} - {unsafe:$condition->getProcessor()->getHtml()} - {/foreach} - - {if $conditionGroup != 'userOptions'} -
- {/if} +
+

{lang}wcf.user.condition.conditionGroup.{$conditionGroup}{/lang}

+ + {foreach from=$conditionObjectTypes item='condition'} + {unsafe:$condition->getProcessor()->getHtml()} + {/foreach} +
{/foreach}
diff --git a/com.woltlab.wcf/templates/shared_userOptionsCondition.tpl b/com.woltlab.wcf/templates/shared_userOptionsCondition.tpl deleted file mode 100644 index 312a2960d28..00000000000 --- a/com.woltlab.wcf/templates/shared_userOptionsCondition.tpl +++ /dev/null @@ -1,10 +0,0 @@ -{foreach from=$optionTree item='category'} -
-
-

{lang}wcf.user.option.category.{$category[object]->categoryName}{/lang}

- {hascontent}

{content}{lang __optional=true}wcf.user.option.category.{$category[object]->categoryName}.description{/lang}{/content}

{/hascontent} -
- - {include file='optionFieldList' options=$category[options] langPrefix='wcf.user.option.'} -
-{/foreach} diff --git a/com.woltlab.wcf/templates/textSearchableOptionType.tpl b/com.woltlab.wcf/templates/textSearchableOptionType.tpl deleted file mode 100644 index 9bfdfbaa309..00000000000 --- a/com.woltlab.wcf/templates/textSearchableOptionType.tpl +++ /dev/null @@ -1,35 +0,0 @@ - -required} required{/if}> - - diff --git a/com.woltlab.wcf/templates/textareaSearchableOptionType.tpl b/com.woltlab.wcf/templates/textareaSearchableOptionType.tpl deleted file mode 100644 index d98eb4d79fc..00000000000 --- a/com.woltlab.wcf/templates/textareaSearchableOptionType.tpl +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/com.woltlab.wcf/templates/user.tpl b/com.woltlab.wcf/templates/user.tpl index 7df99f62937..099fbce7918 100644 --- a/com.woltlab.wcf/templates/user.tpl +++ b/com.woltlab.wcf/templates/user.tpl @@ -3,60 +3,12 @@ {capture assign='headContent'} {event name='javascriptInclude'} diff --git a/com.woltlab.wcf/templates/userCard.tpl b/com.woltlab.wcf/templates/userCard.tpl index b8197b41f69..c2fae04ad7a 100644 --- a/com.woltlab.wcf/templates/userCard.tpl +++ b/com.woltlab.wcf/templates/userCard.tpl @@ -101,8 +101,8 @@ {/if} {foreach from=$user->getUserCardOptions() item=userCardOption} -
{$userCardOption->getTitle()}
-
{unsafe:$userCardOption->optionValue}
+
{$userCardOption[title]}
+
{unsafe:$userCardOption[value]}
{/foreach} {event name='afterDetails'} diff --git a/com.woltlab.wcf/templates/userOptionFieldList.tpl b/com.woltlab.wcf/templates/userOptionFieldList.tpl deleted file mode 100644 index 4af1dcf5bde..00000000000 --- a/com.woltlab.wcf/templates/userOptionFieldList.tpl +++ /dev/null @@ -1,19 +0,0 @@ -{foreach from=$options item=optionData} - {assign var=option value=$optionData[object]} -
- {if $isSearchMode|empty || !$optionData[hideLabelInSearch]}{if $isSearchMode|empty && $option->required} *{/if}{/if} -
{unsafe:$optionData[html]} - {$option->getDescription()} - - {if $errorType|is_array && $errorType[$option->optionName]|isset} - - {if $errorType[$option->optionName] == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}{$langPrefix}error.{$errorType[$option->optionName]}{/lang} - {/if} - - {/if} -
-
-{/foreach} diff --git a/com.woltlab.wcf/templates/userProfileAbout.tpl b/com.woltlab.wcf/templates/userProfileAbout.tpl index bcf9db8681a..db174239885 100644 --- a/com.woltlab.wcf/templates/userProfileAbout.tpl +++ b/com.woltlab.wcf/templates/userProfileAbout.tpl @@ -2,19 +2,17 @@ {content} {event name='beforeUserOptions'} - {foreach from=$options item=category} - {foreach from=$category[categories] item=optionCategory} -
-

{lang}wcf.user.option.category.{$optionCategory[object]->categoryName}{/lang}

- - {foreach from=$optionCategory[options] item=userOption} -
-
{$userOption[object]->getTitle()}
-
{unsafe:$userOption[object]->optionValue}
-
- {/foreach} -
- {/foreach} + {foreach from=$view->getOptionValuesOutput() key=categoryName item=userOptions} +
+

{$view->getCategoryTitle($categoryName)}

+ + {foreach from=$userOptions item=userOption} +
+
{$userOption[object]->getTitle()}
+
{unsafe:$userOption[output]}
+
+ {/foreach} +
{/foreach} {event name='afterUserOptions'} diff --git a/com.woltlab.wcf/templates/userProfileAboutEditable.tpl b/com.woltlab.wcf/templates/userProfileAboutEditable.tpl deleted file mode 100644 index 323b624d376..00000000000 --- a/com.woltlab.wcf/templates/userProfileAboutEditable.tpl +++ /dev/null @@ -1,29 +0,0 @@ -{foreach from=$optionTree item=categoryLevel1} - {foreach from=$categoryLevel1[categories] item=categoryLevel2} -
-

{lang}wcf.user.option.category.{$categoryLevel2[object]->categoryName}{/lang}

- - {if $categoryLevel2[object]->categoryName == 'profile.personal' && MODULE_USER_RANK && $__wcf->session->getPermission('user.profile.canEditUserTitle')} -
-
-
- - {if $errorType[__userTitle]|isset} - - {lang}wcf.user.userTitle.error.{$errorType[__userTitle]}{/lang} - - {/if} - {lang}wcf.user.userTitle.description{/lang} -
-
- {/if} - - {include file='userProfileOptionFieldList' options=$categoryLevel2[options] langPrefix='wcf.user.option.'} -
- {/foreach} -{/foreach} - -
- - -
diff --git a/com.woltlab.wcf/templates/userProfileHeader.tpl b/com.woltlab.wcf/templates/userProfileHeader.tpl index c89e47c2d6b..6a53e393e18 100644 --- a/com.woltlab.wcf/templates/userProfileHeader.tpl +++ b/com.woltlab.wcf/templates/userProfileHeader.tpl @@ -1,23 +1,6 @@
isInAccessibleGroup()} - {if $__wcf->session->getPermission('admin.user.canBanUser')} - data-banned="{$view->user->banned}" - {/if} - {if $__wcf->session->getPermission('admin.user.canDisableAvatar')} - data-disable-avatar="{$view->user->disableAvatar}" - {/if} - {if $__wcf->session->getPermission('admin.user.canDisableSignature')} - data-disable-signature="{$view->user->disableSignature}" - {/if} - {if $__wcf->session->getPermission('admin.user.canDisableCoverPhoto')} - data-disable-cover-photo="{$view->user->disableCoverPhoto}" - {/if} - {if $__wcf->session->getPermission('admin.user.canEnableUser')} - data-is-disabled="{if $view->user->activationCode}true{else}false{/if}" - {/if} - {/if} >
@@ -46,7 +29,7 @@ {/if} {if $view->canEditUser()} - @@ -72,7 +55,7 @@
{if $view->user->banned} - + {icon name='lock'} {/if} diff --git a/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl b/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl deleted file mode 100644 index 6d654a1b5a2..00000000000 --- a/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl +++ /dev/null @@ -1,23 +0,0 @@ -{foreach from=$options item=optionData} - {assign var=option value=$optionData[object]} - {if $errorType|is_array && $errorType[$option->optionName]|isset} - {assign var=error value=$errorType[$option->optionName]} - {else} - {assign var=error value=''} - {/if} -
- -
{unsafe:$optionData[html]} - {if $error} - - {if $error == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}{$langPrefix}error.{$error}{/lang} - {/if} - - {/if} - {$option->getDescription()} -
-
-{/foreach} diff --git a/com.woltlab.wcf/templates/userSearch.tpl b/com.woltlab.wcf/templates/userSearch.tpl deleted file mode 100644 index f48b5dd2dc7..00000000000 --- a/com.woltlab.wcf/templates/userSearch.tpl +++ /dev/null @@ -1,47 +0,0 @@ -{include file='header'} - -{if $errorField == 'search'} - {lang}wcf.user.search.error.noMatches{/lang} -{else} - {include file='shared_formError'} -{/if} - -
-
-
-
-
- -
-
- - {event name='generalFields'} -
- - {if !$optionTree|empty} - {foreach from=$optionTree[0][categories] item=category} -
-
-

{lang}wcf.user.option.category.{$category[object]->categoryName}{/lang}

- {hascontent}

{content}{lang __optional=true}wcf.user.option.category.{$category[object]->categoryName}.description{/lang}{/content}

{/hascontent} -
- {include file='userOptionFieldList' options=$category[options] langPrefix='wcf.user.option.' isSearchMode=true} -
- {/foreach} - {/if} - - {event name='sections'} - -
- - {csrfToken} -
-
- - - -{include file='footer'} diff --git a/com.woltlab.wcf/userOption.xml b/com.woltlab.wcf/userOption.xml index 61ad8e762a9..fefba5329f8 100644 --- a/com.woltlab.wcf/userOption.xml +++ b/com.woltlab.wcf/userOption.xml @@ -51,16 +51,13 @@ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 64fa3f6f964..877d3a461cb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 2 - path: wcfsetup/install/files/lib/acp/action/UserExportGdprAction.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -42,7 +36,6 @@ parameters: count: 2 path: wcfsetup/install/files/lib/acp/form/AbstractCustomOptionForm.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -169,18 +162,6 @@ parameters: count: 4 path: wcfsetup/install/files/lib/acp/form/TrophyEditForm.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 5 - path: wcfsetup/install/files/lib/acp/form/UserAddForm.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 5 - path: wcfsetup/install/files/lib/acp/form/UserEditForm.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -253,12 +234,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/acp/page/TemplateDiffPage.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/acp/page/UserListPage.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -337,12 +312,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/data/I18nDatabaseObjectList.class.php - - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: wcfsetup/install/files/lib/data/TCollectionUserProfiles.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -646,7 +615,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed - count: 10 + count: 8 path: wcfsetup/install/files/lib/data/user/UserAction.class.php - @@ -661,18 +630,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/data/user/UserList.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/data/user/UserProfile.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/data/user/UserProfileAction.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -715,6 +672,12 @@ parameters: count: 1 path: wcfsetup/install/files/lib/data/user/profile/menu/item/UserProfileMenuItemEditor.class.php + - + message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: wcfsetup/install/files/lib/data/user/profile/visitor/UserProfileVisitorCollection.class.php + - message: '#^Instanceof between wcf\\data\\user\\profile\\visitor\\UserProfileVisitor and wcf\\data\\DatabaseObject will always evaluate to true\.$#' identifier: instanceof.alwaysTrue @@ -763,12 +726,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/form/NotificationUnsubscribeForm.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 4 - path: wcfsetup/install/files/lib/form/RegisterForm.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -793,12 +750,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/form/TagSearchForm.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/form/UserSearchForm.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -817,12 +768,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/page/ContactAttachmentPage.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/page/MembersListPage.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -1201,12 +1146,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/system/clipboard/action/AbstractClipboardAction.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/clipboard/action/UserClipboardAction.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -1363,12 +1302,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/system/condition/UserMultifactorCondition.class.php - - - message: '#^Method wcf\\system\\condition\\UserOptionsCondition\:\:getData\(\) should return array\ but returns null\.$#' - identifier: return.type - count: 1 - path: wcfsetup/install/files/lib/system/condition/UserOptionsCondition.class.php - - message: '#^Method wcf\\system\\condition\\UserRegistrationDateCondition\:\:getData\(\) should return array\ but returns null\.$#' identifier: return.type @@ -1525,12 +1458,6 @@ parameters: count: 1 path: wcfsetup/install/files/lib/system/email/mime/RecipientAwareTextMimePart.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 3 - path: wcfsetup/install/files/lib/system/event/listener/AbstractUserMergeListener.class.php - - message: '#^Constructor of class wcf\\system\\exception\\ErrorException has an unused parameter \$filename\.$#' identifier: constructor.unusedParameter @@ -2197,66 +2124,6 @@ parameters: count: 2 path: wcfsetup/install/files/lib/system/option/UseroptionsOptionType.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/option/user/DateUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/option/user/FacebookUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/option/user/FloatUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/option/user/ImageUserOptionOutput.class.php - - - - message: '#^Strict comparison using \=\=\= between string and null will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: wcfsetup/install/files/lib/system/option/user/MessageUserOptionOutput.class.php - - - - message: '#^Strict comparison using \=\=\= between string and null will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: wcfsetup/install/files/lib/system/option/user/NewlineToBreakUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 2 - path: wcfsetup/install/files/lib/system/option/user/SelectOptionsUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/option/user/TwitterUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/option/user/URLUserOptionOutput.class.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 3 - path: wcfsetup/install/files/lib/system/option/user/UserOptionHandler.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -2527,12 +2394,6 @@ parameters: count: 2 path: wcfsetup/install/files/lib/system/package/plugin/UserNotificationEventPackageInstallationPlugin.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/package/plugin/UserOptionPackageInstallationPlugin.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed diff --git a/ts/WoltLabSuite/Core/Acp/Component/Tag/SynonymsHandler.ts b/ts/WoltLabSuite/Core/Acp/Component/Tag/SynonymsHandler.ts deleted file mode 100644 index 504178bb456..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Component/Tag/SynonymsHandler.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Handles the dialog to set tags as synonyms. - * - * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - */ - -import { add as addEvent } from "WoltLabSuite/Core/Event/Handler"; -import { ClipboardActionData } from "WoltLabSuite/Core/Controller/Clipboard/Data"; -import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import DomUtil from "WoltLabSuite/Core/Dom/Util"; -import { dboAction } from "WoltLabSuite/Core/Ajax"; - -export function init() { - addEvent("com.woltlab.wcf.clipboard", "com.woltlab.wcf.tag", (actionData: { data: ClipboardActionData }) => { - if (actionData.data.actionName === "com.woltlab.wcf.tag.setAsSynonyms") { - openDialog(actionData.data.parameters.objectIDs, actionData.data.parameters.template); - } - }); -} - -function openDialog(objectIDs: number[], template: string) { - const dialog = dialogFactory().fromHtml(template).asConfirmation(); - dialog.addEventListener("validate", (event) => { - const checked = dialog.querySelectorAll("input[type=radio]:checked").length > 0; - event.detail.push(Promise.resolve(checked)); - - DomUtil.innerError( - dialog.querySelector(".containerBoxList")!, - checked ? undefined : getPhrase("wcf.global.form.error.empty"), - ); - }); - dialog.addEventListener("primary", () => { - void dboAction("setAsSynonyms", "wcf\\data\\tag\\TagAction") - .objectIds(objectIDs) - .payload({ - tagID: dialog.querySelector('input[name="tagID"]:checked')!.value, - }) - .dispatch() - .then(() => { - window.location.reload(); - }); - }); - - dialog.show(getPhrase("wcf.acp.tag.setAsSynonyms")); -} diff --git a/ts/WoltLabSuite/Core/Acp/Component/User/EmailAddressExport.ts b/ts/WoltLabSuite/Core/Acp/Component/User/EmailAddressExport.ts new file mode 100644 index 00000000000..548ad837145 --- /dev/null +++ b/ts/WoltLabSuite/Core/Acp/Component/User/EmailAddressExport.ts @@ -0,0 +1,52 @@ +/** + * Exports the email addresses of one or more users and offers the resulting + * file for download. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; + +type Result = { + filename: string; + mimeType: string; + content: string; +}; + +async function exportEmailAddresses(endpoint: string): Promise { + const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(endpoint); + if (!ok) { + return false; + } + + const url = window.URL.createObjectURL(new Blob([result.content], { type: `${result.mimeType};charset=utf-8` })); + + const link = document.createElement("a"); + link.href = url; + link.download = result.filename; + link.click(); + + // Revoking the URL synchronously can abort the download in some browsers. + window.setTimeout(() => window.URL.revokeObjectURL(url), 0); + + return true; +} + +export function setupBulk(identifier: string, container: HTMLElement): void { + container.addEventListener("bulk-interaction", (event: CustomEvent>) => { + if (event.detail.bulkInteraction !== identifier) { + return; + } + + void exportEmailAddresses(event.detail.endpoint).then((ok) => { + if (!ok) { + return; + } + + container.dispatchEvent(new CustomEvent("interaction:reset-selection")); + }); + }); +} diff --git a/ts/WoltLabSuite/Core/Acp/Component/User/ProfileHeader.ts b/ts/WoltLabSuite/Core/Acp/Component/User/ProfileHeader.ts new file mode 100644 index 00000000000..892331f14d2 --- /dev/null +++ b/ts/WoltLabSuite/Core/Acp/Component/User/ProfileHeader.ts @@ -0,0 +1,47 @@ +/** + * Refreshes the user profile header on the user edit page after an + * interaction has changed the state of the user. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { getAdminUserProfileHeader } from "WoltLabSuite/Core/Api/Users/GetAdminUserProfileHeader"; + +type Payload = Record; + +async function refreshHeader(header: HTMLElement, userId: number): Promise { + const { template } = await getAdminUserProfileHeader(userId); + + const container = document.createElement("template"); + container.innerHTML = template; + + const newHeader = container.content.querySelector(".userProfileHeader"); + if (!newHeader) { + return; + } + + // The buttons contain the interaction context menu, which refreshes itself. + // Keeping them preserves the initialized dropdown and its event listeners. + const buttons = header.querySelector(".userProfileHeader__buttons"); + const newButtons = newHeader.querySelector(".userProfileHeader__buttons"); + if (buttons && newButtons) { + newButtons.replaceWith(buttons); + } + + header.replaceChildren(...newHeader.childNodes); +} + +export function setup(header: HTMLElement): void { + const userId = parseInt(header.dataset.objectId!); + + header.addEventListener("interaction:invalidate", (event: CustomEvent) => { + if (event.detail._reloadPage === "true") { + return; + } + + void refreshHeader(header, userId); + }); +} diff --git a/ts/WoltLabSuite/Core/Acp/Component/User/RemoveContent.ts b/ts/WoltLabSuite/Core/Acp/Component/User/RemoveContent.ts new file mode 100644 index 00000000000..1f3c528014c --- /dev/null +++ b/ts/WoltLabSuite/Core/Acp/Component/User/RemoveContent.ts @@ -0,0 +1,92 @@ +/** + * Selects the content of one or more users in a dialog and removes it with + * the `UserContentRemoveWorker`. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import AcpUiWorker from "WoltLabSuite/Core/Acp/Ui/Worker"; +import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; +import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; +import { getPhrase } from "WoltLabSuite/Core/Language"; + +type Result = { + userIDs: number[]; + contentProviders: string[]; +}; + +async function removeContent(endpoint: string): Promise { + const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(endpoint); + if (!ok) { + return false; + } + + return new Promise((resolve) => { + new AcpUiWorker({ + dialogId: "removeContentWorker", + dialogTitle: getPhrase("wcf.acp.content.removeContent"), + className: "wcf\\system\\worker\\UserContentRemoveWorker", + parameters: { + userIDs: result.userIDs, + contentProvider: result.contentProviders, + }, + callbackAbort: () => resolve(false), + callbackSuccess: () => resolve(true), + }); + }); +} + +export function setup(identifier: string, container: HTMLElement): void { + container.addEventListener("interaction:execute", (event: CustomEvent>) => { + if (event.detail.interaction !== identifier) { + return; + } + + const element = event.target as HTMLElement; + void removeContent(event.detail.endpoint).then((ok) => { + if (!ok) { + return; + } + + // The activity points and the number of likes received may have changed. + element.dispatchEvent( + new CustomEvent("interaction:invalidate", { + bubbles: true, + detail: event.detail, + }), + ); + + showDefaultSuccessSnackbar(); + }); + }); +} + +export function setupBulk(identifier: string, container: HTMLElement): void { + container.addEventListener("bulk-interaction", (event: CustomEvent>) => { + if (event.detail.bulkInteraction !== identifier) { + return; + } + + const objectIds = JSON.parse(event.detail.objectIds) as number[]; + void removeContent(event.detail.endpoint).then((ok) => { + if (!ok) { + return; + } + + for (const objectId of objectIds) { + container.querySelector(`[data-object-id="${objectId}"]`)?.dispatchEvent( + new CustomEvent("interaction:invalidate", { + bubbles: true, + }), + ); + } + + showDefaultSuccessSnackbar(); + + container.dispatchEvent(new CustomEvent("interaction:reset-selection")); + }); + }); +} diff --git a/ts/WoltLabSuite/Core/Acp/Controller/User/List.ts b/ts/WoltLabSuite/Core/Acp/Controller/User/List.ts deleted file mode 100644 index c8cd6504a55..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Controller/User/List.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Handles the list of users. - * - * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - */ - -import * as EventHandler from "../../../Event/Handler"; -import { ClipboardActionData } from "WoltLabSuite/Core/Controller/Clipboard/Data"; -import { dboAction } from "WoltLabSuite/Core/Ajax"; -import UiDropdownSimple from "WoltLabSuite/Core/Ui/Dropdown/Simple"; -import BanHandler from "WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban"; -import SendNewPassword from "WoltLabSuite/Core/Acp/Ui/User/Action/Handler/SendNewPassword"; -import { setup as setupClipboard, unmark as unmarkClipboard } from "WoltLabSuite/Core/Controller/Clipboard"; -import AcpUiUserList from "WoltLabSuite/Core/Acp/Ui/User/Editor"; -import { AcpUserContentRemoveClipboard } from "WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Clipboard"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; - -function getUserElements(userIDs: number[]): HTMLElement[] { - return Array.from(document.querySelectorAll(".jsUserRow")).filter((userRow) => - userIDs.includes(parseInt(userRow.dataset.objectId!)), - ); -} - -function getDropdownMenu(userRow: HTMLElement): HTMLElement { - const userId = ~~userRow.dataset.objectId!; - const dropdownId = `userListDropdown${userId}`; - return UiDropdownSimple.getDropdownMenu(dropdownId)!; -} - -function refresh(userIDs: number[]) { - unmarkClipboard("com.woltlab.wcf.user", userIDs); - - showDefaultSuccessSnackbar(); - - EventHandler.fire("com.woltlab.wcf.acp.user", "refresh", { - userIds: userIDs, - }); -} - -async function enableUsers(userIDs: number[]) { - await dboAction("enable", "wcf\\data\\user\\UserAction").objectIds(userIDs).dispatch(); - - getUserElements(userIDs).forEach((userRow) => { - userRow.dataset.enabled = "true"; - const button = getDropdownMenu(userRow).querySelector(".jsEnable")!; - button.textContent = button.dataset.disableMessage!; - }); - - refresh(userIDs); -} - -function banUsers(userIDs: number[]) { - new BanHandler(userIDs).ban(() => { - getUserElements(userIDs).forEach((userRow) => { - userRow.dataset.banned = "true"; - - const button = getDropdownMenu(userRow).querySelector(".jsBan")!; - button.textContent = button.dataset.unbanMessage!; - - refresh(userIDs); - }); - }); -} - -function sendNewPasswords(userIDs: number[]) { - new SendNewPassword(userIDs, () => { - refresh(userIDs); - }).send(); -} - -function setupUserClipboard(hasMarkedItems: boolean) { - setupClipboard({ - pageClassName: "wcf\\acp\\page\\UserListPage", - hasMarkedItems: hasMarkedItems, - }); - - EventHandler.add("com.woltlab.wcf.clipboard", "com.woltlab.wcf.user", (data: { data: ClipboardActionData }) => { - switch (data.data.actionName) { - case "com.woltlab.wcf.user.enable": - void enableUsers(data.data.parameters.objectIDs); - break; - case "com.woltlab.wcf.user.ban": - banUsers(data.data.parameters.objectIDs); - break; - case "com.woltlab.wcf.user.sendNewPassword": - sendNewPasswords(data.data.parameters.objectIDs); - break; - } - }); - - new AcpUserContentRemoveClipboard(); -} - -export function setup(hasMarkedItems: boolean) { - setupUserClipboard(hasMarkedItems); - - new AcpUiUserList(); -} diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Abstract.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Abstract.ts deleted file mode 100644 index 6cb507b310e..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Abstract.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * An abstract action, to handle user actions. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -export abstract class AbstractUserAction { - protected readonly button: HTMLElement; - protected readonly userDataElement: HTMLElement; - protected readonly userId: number; - - public constructor(button: HTMLElement, userId: number, userDataElement: HTMLElement) { - this.button = button; - this.userId = userId; - this.userDataElement = userDataElement; - } -} - -export default AbstractUserAction; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/BanAction.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/BanAction.ts deleted file mode 100644 index d9ed4c479f5..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/BanAction.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Handles a user ban button. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import * as Core from "../../../../Core"; -import AbstractUserAction from "./Abstract"; -import BanHandler from "./Handler/Ban"; -import * as EventHandler from "../../../../Event/Handler"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; - -export class BanAction extends AbstractUserAction { - private banHandler: BanHandler; - - public constructor(button: HTMLElement, userId: number, userDataElement: HTMLElement) { - super(button, userId, userDataElement); - - this.banHandler = new BanHandler([this.userId]); - - this.button.addEventListener("click", (event) => { - event.preventDefault(); - - const isBanned = Core.stringToBool(this.userDataElement.dataset.banned!); - - if (isBanned) { - this.banHandler.unban(() => { - this.userDataElement.dataset.banned = "false"; - this.button.textContent = this.button.dataset.banMessage!; - - showDefaultSuccessSnackbar(); - - EventHandler.fire("com.woltlab.wcf.acp.user", "refresh", { - userIds: [this.userId], - }); - }); - } else { - this.banHandler.ban(() => { - this.userDataElement.dataset.banned = "true"; - this.button.textContent = this.button.dataset.unbanMessage!; - - showDefaultSuccessSnackbar(); - - EventHandler.fire("com.woltlab.wcf.acp.user", "refresh", { - userIds: [this.userId], - }); - }); - } - }); - } -} - -export default BanAction; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/DeleteAction.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/DeleteAction.ts deleted file mode 100644 index f75c256b500..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/DeleteAction.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Handles a user delete button. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import AbstractUserAction from "./Abstract"; -import Delete from "./Handler/Delete"; - -export class DeleteAction extends AbstractUserAction { - public constructor(button: HTMLElement, userId: number, userDataElement: HTMLElement) { - super(button, userId, userDataElement); - - if (typeof this.button.dataset.confirmMessage !== "string") { - throw new Error("The button does not provide a confirmMessage."); - } - - this.button.addEventListener("click", (event) => { - event.preventDefault(); - - const deleteHandler = new Delete( - [this.userId], - () => { - this.userDataElement.remove(); - }, - this.button.dataset.confirmMessage!, - ); - deleteHandler.delete(); - }); - } -} - -export default DeleteAction; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/DisableAction.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/DisableAction.ts deleted file mode 100644 index 146c136c86a..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/DisableAction.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Handles a user disable/enable button. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import * as Ajax from "../../../../Ajax"; -import * as Core from "../../../../Core"; -import { AjaxCallbackObject, AjaxCallbackSetup, DatabaseObjectActionResponse } from "../../../../Ajax/Data"; -import AbstractUserAction from "./Abstract"; -import * as EventHandler from "../../../../Event/Handler"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; - -export class DisableAction extends AbstractUserAction implements AjaxCallbackObject { - public constructor(button: HTMLElement, userId: number, userDataElement: HTMLElement) { - super(button, userId, userDataElement); - - this.button.addEventListener("click", (event) => { - event.preventDefault(); - const isEnabled = Core.stringToBool(this.userDataElement.dataset.enabled!); - - Ajax.api(this, { - actionName: isEnabled ? "disable" : "enable", - }); - }); - } - - _ajaxSetup(): ReturnType { - return { - data: { - className: "wcf\\data\\user\\UserAction", - objectIDs: [this.userId], - }, - }; - } - - _ajaxSuccess(data: DatabaseObjectActionResponse): void { - data.objectIDs.forEach((objectId) => { - if (~~objectId == this.userId) { - switch (data.actionName) { - case "enable": - this.userDataElement.dataset.enabled = "true"; - this.button.textContent = this.button.dataset.disableMessage!; - break; - - case "disable": - this.userDataElement.dataset.enabled = "false"; - this.button.textContent = this.button.dataset.enableMessage!; - break; - - default: - throw new Error("Unreachable"); - } - } - }); - - showDefaultSuccessSnackbar(); - - EventHandler.fire("com.woltlab.wcf.acp.user", "refresh", { - userIds: [this.userId], - }); - } -} - -export default DisableAction; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban.ts deleted file mode 100644 index aeccca877c6..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Handles a user ban. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import * as Ajax from "../../../../../Ajax"; -import BanDialog from "./Ban/Dialog"; - -type Callback = () => void; - -export class BanHandler { - private userIDs: number[]; - - public constructor(userIDs: number[]) { - this.userIDs = userIDs; - } - - public ban(callback: Callback): void { - BanDialog.open(this.userIDs, callback); - } - - public unban(callback: Callback): void { - Ajax.api({ - _ajaxSetup: () => { - return { - data: { - actionName: "unban", - className: "wcf\\data\\user\\UserAction", - objectIDs: this.userIDs, - }, - }; - }, - _ajaxSuccess: () => callback(), - }); - } -} - -export default BanHandler; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban/Dialog.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban/Dialog.ts deleted file mode 100644 index 10946cdc918..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban/Dialog.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Creates and handles the dialog to ban a user. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import UiDialog from "../../../../../../Ui/Dialog"; -import { DialogCallbackSetup } from "../../../../../../Ui/Dialog/Data"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import * as Ajax from "../../../../../../Ajax"; -import DatePicker from "../../../../../../Date/Picker"; - -type Callback = () => void; - -export class BanDialog { - private static instance: BanDialog; - - private banCallback: Callback; - private userIDs: number[]; - private submitElement: HTMLElement; - private neverExpiresCheckbox: HTMLInputElement; - private reasonInput: HTMLInputElement; - private userBanExpiresSettingsElement: HTMLElement; - private dialogContent: HTMLElement; - - public static open(userIDs: number[], callback: Callback): void { - if (!BanDialog.instance) { - BanDialog.instance = new BanDialog(); - } - - BanDialog.instance.setCallback(callback); - BanDialog.instance.setUserIDs(userIDs); - BanDialog.instance.openDialog(); - } - - private openDialog(): void { - UiDialog.open(this); - } - - private setCallback(callback: Callback): void { - this.banCallback = callback; - } - - private setUserIDs(userIDs: number[]): void { - this.userIDs = userIDs; - } - - private banSubmit(reason: string, expires: string): void { - Ajax.apiOnce({ - data: { - actionName: "ban", - className: "wcf\\data\\user\\UserAction", - objectIDs: this.userIDs, - parameters: { - banReason: reason, - banExpires: expires, - }, - }, - success: this.banCallback, - }); - } - - private cleanupDialog(): void { - this.reasonInput.value = ""; - this.neverExpiresCheckbox.checked = true; - DatePicker.clear("userBanExpires"); - this.userBanExpiresSettingsElement.style.setProperty("display", "none", ""); - } - - _dialogSetup(): ReturnType { - return { - id: "userBanHandler", - options: { - onSetup: (content: HTMLElement): void => { - this.dialogContent = content; - this.submitElement = content.querySelector(".formSubmitButton")!; - this.reasonInput = content.querySelector("#userBanReason") as HTMLInputElement; - this.neverExpiresCheckbox = content.querySelector("#userBanNeverExpires") as HTMLInputElement; - this.userBanExpiresSettingsElement = content.querySelector("#userBanExpiresSettings")!; - - this.submitElement.addEventListener("click", (event) => { - event.preventDefault(); - - const expires = this.neverExpiresCheckbox.checked ? "" : DatePicker.getValue("userBanExpires"); - this.banSubmit(this.reasonInput.value, expires); - - UiDialog.close(this); - - this.cleanupDialog(); - }); - - this.neverExpiresCheckbox.addEventListener("change", (event) => { - const checkbox = event.currentTarget as HTMLInputElement; - if (checkbox.checked) { - this.userBanExpiresSettingsElement.style.setProperty("display", "none", ""); - } else { - this.userBanExpiresSettingsElement.style.removeProperty("display"); - } - }); - }, - title: getPhrase("wcf.acp.user.ban.sure"), - }, - source: ` -
-
-
-
- - ${getPhrase("wcf.acp.user.banReason.description")} -
-
-
-
-
- -
-
- -
-
- -
`, - }; - } -} - -export default BanDialog; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Delete.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Delete.ts deleted file mode 100644 index 719e0e5b6c7..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Delete.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Deletes a given user. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import * as UiConfirmation from "../../../../../Ui/Confirmation"; -import * as Ajax from "../../../../../Ajax"; -import { CallbackSuccess } from "../../../../../Ajax/Data"; - -export class Delete { - private userIDs: number[]; - private successCallback: CallbackSuccess; - private deleteMessage: string; - - public constructor(userIDs: number[], successCallback: CallbackSuccess, deleteMessage: string) { - this.userIDs = userIDs; - this.successCallback = successCallback; - this.deleteMessage = deleteMessage; - } - - public delete(): void { - UiConfirmation.show({ - confirm: () => { - Ajax.apiOnce({ - data: { - actionName: "delete", - className: "wcf\\data\\user\\UserAction", - objectIDs: this.userIDs, - }, - success: this.successCallback, - }); - }, - message: this.deleteMessage, - messageIsHtml: true, - }); - } -} - -export default Delete; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/SendNewPassword.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/SendNewPassword.ts index b288c77dc55..8285b76cd84 100644 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/SendNewPassword.ts +++ b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/Handler/SendNewPassword.ts @@ -5,6 +5,7 @@ * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.5 + * @deprecated 6.3 Use the `core/users/{id}/send-new-password` endpoint instead. */ import { getPhrase } from "WoltLabSuite/Core/Language"; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/SendNewPasswordAction.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/SendNewPasswordAction.ts deleted file mode 100644 index 2474a9c6a8a..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/SendNewPasswordAction.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Handles a send new password button. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import AbstractUserAction from "./Abstract"; -import SendNewPassword from "./Handler/SendNewPassword"; - -export class SendNewPasswordAction extends AbstractUserAction { - public constructor(button: HTMLElement, userId: number, userDataElement: HTMLElement) { - super(button, userId, userDataElement); - - this.button.addEventListener("click", (event) => { - event.preventDefault(); - - const sendNewPasswordHandler = new SendNewPassword([this.userId], () => { - location.reload(); - }); - sendNewPasswordHandler.send(); - }); - } -} - -export default SendNewPasswordAction; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/ToggleConfirmEmailAction.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Action/ToggleConfirmEmailAction.ts deleted file mode 100644 index bd712564ced..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Action/ToggleConfirmEmailAction.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Handles a toggle confirm email button. - * - * @author Joshua Ruesweg - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.5 - */ - -import AbstractUserAction from "./Abstract"; -import * as Ajax from "../../../../Ajax"; -import * as Core from "../../../../Core"; -import { AjaxCallbackSetup, DatabaseObjectActionResponse } from "../../../../Ajax/Data"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; - -export class ToggleConfirmEmailAction extends AbstractUserAction { - public constructor(button: HTMLElement, userId: number, userDataElement: HTMLElement) { - super(button, userId, userDataElement); - - this.button.addEventListener("click", (event) => { - event.preventDefault(); - const isEmailConfirmed = Core.stringToBool(this.userDataElement.dataset.emailConfirmed!); - - Ajax.api(this, { - actionName: isEmailConfirmed ? "unconfirmEmail" : "confirmEmail", - }); - }); - } - - _ajaxSetup(): ReturnType { - return { - data: { - className: "wcf\\data\\user\\UserAction", - objectIDs: [this.userId], - }, - }; - } - - _ajaxSuccess(data: DatabaseObjectActionResponse): void { - data.objectIDs.forEach((objectId) => { - if (~~objectId == this.userId) { - switch (data.actionName) { - case "confirmEmail": - this.userDataElement.dataset.emailConfirmed = "true"; - this.button.textContent = this.button.dataset.unconfirmEmailMessage!; - break; - - case "unconfirmEmail": - this.userDataElement.dataset.emailConfirmed = "false"; - this.button.textContent = this.button.dataset.confirmEmailMessage!; - break; - - default: - throw new Error("Unreachable"); - } - } - }); - - showDefaultSuccessSnackbar(); - } -} - -export default ToggleConfirmEmailAction; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Clipboard.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Clipboard.ts deleted file mode 100644 index 1f02d3969c1..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Clipboard.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Handles the user content remove clipboard action. - * - * @author Joshua Ruesweg - * @copyright 2001-2020 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.4 - */ - -import AcpUiWorker from "../../../Worker"; -import * as Ajax from "../../../../../Ajax"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import UiDialog from "../../../../../Ui/Dialog"; -import { AjaxCallbackSetup } from "../../../../../Ajax/Data"; -import { DialogCallbackSetup } from "../../../../../Ui/Dialog/Data"; -import * as EventHandler from "../../../../../Event/Handler"; - -interface AjaxResponse { - returnValues: { - template: string; - }; -} - -interface EventData { - data: { - actionName: string; - internalData: any[]; - label: string; - parameters: { - objectIDs: number[]; - url: string; - }; - }; - listItem: HTMLElement; -} - -export class AcpUserContentRemoveClipboard { - public userIds: number[]; - private readonly dialogId = "userContentRemoveClipboardPrepareDialog"; - - /** - * Initializes the content remove handler. - */ - constructor() { - EventHandler.add("com.woltlab.wcf.clipboard", "com.woltlab.wcf.user", (data: EventData) => { - if (data.data.actionName === "com.woltlab.wcf.user.deleteUserContent") { - this.userIds = data.data.parameters.objectIDs; - - Ajax.api(this); - } - }); - } - - /** - * Executes the remove content worker. - */ - private executeWorker(objectTypes: string[]): void { - new AcpUiWorker({ - // dialog - dialogId: "removeContentWorker", - dialogTitle: getPhrase("wcf.acp.content.removeContent"), - - // ajax - className: "wcf\\system\\worker\\UserContentRemoveWorker", - parameters: { - userIDs: this.userIds, - contentProvider: objectTypes, - }, - }); - } - - /** - * Handles a click on the submit button in the overlay. - */ - private submit(): void { - const objectTypes = Array.from( - this.dialogContent.querySelectorAll("input.contentProviderObjectType"), - ) - .filter((element) => element.checked) - .map((element) => element.name); - - UiDialog.close(this.dialogId); - - if (objectTypes.length > 0) { - window.setTimeout(() => { - this.executeWorker(objectTypes); - }, 200); - } - } - - get dialogContent(): HTMLElement { - return UiDialog.getDialog(this.dialogId)!.content; - } - - _ajaxSuccess(data: AjaxResponse): void { - UiDialog.open(this, data.returnValues.template); - - const submitButton = this.dialogContent.querySelector('input[type="submit"]') as HTMLElement; - submitButton.addEventListener("click", () => this.submit()); - } - - _ajaxSetup(): ReturnType { - return { - data: { - actionName: "prepareRemoveContent", - className: "wcf\\data\\user\\UserAction", - parameters: { - userIDs: this.userIds, - }, - }, - }; - } - - _dialogSetup(): ReturnType { - return { - id: this.dialogId, - options: { - title: getPhrase("wcf.acp.content.removeContent"), - }, - source: null, - }; - } -} - -export default AcpUserContentRemoveClipboard; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Handler.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Handler.ts deleted file mode 100644 index ebdc312936a..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Handler.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Provides the trophy icon designer. - * - * @author Joshua Ruesweg - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.2 - */ - -import AcpUiWorker from "../../../Worker"; -import * as Ajax from "../../../../../Ajax"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import UiDialog from "../../../../../Ui/Dialog"; -import { AjaxCallbackSetup } from "../../../../../Ajax/Data"; -import { DialogCallbackSetup } from "../../../../../Ui/Dialog/Data"; - -type CallbackSuccess = (data: AjaxResponseSuccess) => void; - -interface AjaxResponseSuccess { - loopCount: number; - parameters: ArbitraryObject; - proceedURL: string; - progress: number; - template?: string; -} - -interface AjaxResponse { - returnValues: { - template: string; - }; -} - -class AcpUserContentRemoveHandler { - private readonly dialogId: string; - private readonly userId: number; - private readonly callbackSuccess?: CallbackSuccess; - - /** - * Initializes the content remove handler. - */ - constructor(element: HTMLElement, userId: number, callbackSuccess?: CallbackSuccess) { - this.userId = userId; - this.dialogId = `userRemoveContentHandler-${this.userId}`; - this.callbackSuccess = callbackSuccess; - - element.addEventListener("click", (ev) => this.click(ev)); - } - - /** - * Click on the remove content button. - */ - private click(event: MouseEvent): void { - event.preventDefault(); - - Ajax.api(this); - } - - /** - * Executes the remove content worker. - */ - private executeWorker(objectTypes: string[]): void { - new AcpUiWorker({ - // dialog - dialogId: "removeContentWorker", - dialogTitle: getPhrase("wcf.acp.content.removeContent"), - - // ajax - className: "\\wcf\\system\\worker\\UserContentRemoveWorker", - parameters: { - userID: this.userId, - contentProvider: objectTypes, - }, - callbackSuccess: this.callbackSuccess, - }); - } - - /** - * Handles a click on the submit button in the overlay. - */ - private submit(): void { - const objectTypes = Array.from( - this.dialogContent.querySelectorAll("input.contentProviderObjectType"), - ) - .filter((element) => element.checked) - .map((element) => element.name); - - UiDialog.close(this.dialogId); - - if (objectTypes.length > 0) { - window.setTimeout(() => { - this.executeWorker(objectTypes); - }, 200); - } - } - - get dialogContent(): HTMLElement { - return UiDialog.getDialog(this.dialogId)!.content; - } - - _ajaxSuccess(data: AjaxResponse): void { - UiDialog.open(this, data.returnValues.template); - - const submitButton = this.dialogContent.querySelector('input[type="submit"]') as HTMLElement; - submitButton.addEventListener("click", () => this.submit()); - } - - _ajaxSetup(): ReturnType { - return { - data: { - actionName: "prepareRemoveContent", - className: "wcf\\data\\user\\UserAction", - parameters: { - userID: this.userId, - }, - }, - }; - } - - _dialogSetup(): ReturnType { - return { - id: this.dialogId, - options: { - title: getPhrase("wcf.acp.content.removeContent"), - }, - source: null, - }; - } -} - -export = AcpUserContentRemoveHandler; diff --git a/ts/WoltLabSuite/Core/Acp/Ui/User/Editor.ts b/ts/WoltLabSuite/Core/Acp/Ui/User/Editor.ts deleted file mode 100644 index b8626cbdf0f..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/User/Editor.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * User editing capabilities for the user list. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - */ - -import AcpUserContentRemoveHandler from "./Content/Remove/Handler"; -import * as Core from "../../../Core"; -import * as EventHandler from "../../../Event/Handler"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import UiDropdownSimple from "../../../Ui/Dropdown/Simple"; -import DomUtil from "../../../Dom/Util"; -import SendNewPasswordAction from "./Action/SendNewPasswordAction"; -import ToggleConfirmEmailAction from "./Action/ToggleConfirmEmailAction"; -import DisableAction from "./Action/DisableAction"; -import BanAction from "./Action/BanAction"; -import DeleteAction from "./Action/DeleteAction"; - -interface RefreshUsersData { - userIds: number[]; -} - -class AcpUiUserEditor { - /** - * Initializes the edit dropdown for each user. - */ - constructor() { - document.querySelectorAll(".jsUserRow").forEach((userRow: HTMLTableRowElement) => this.initUser(userRow)); - - EventHandler.add("com.woltlab.wcf.acp.user", "refresh", (data: RefreshUsersData) => this.refreshUsers(data)); - } - - /** - * Initializes the edit dropdown for a user. - */ - private initUser(userRow: HTMLTableRowElement): void { - const userId = ~~userRow.dataset.objectId!; - const dropdownId = `userListDropdown${userId}`; - const dropdownMenu = UiDropdownSimple.getDropdownMenu(dropdownId)!; - const legacyButtonContainer = userRow.querySelector(".jsLegacyButtons") as HTMLElement; - - if (dropdownMenu.childElementCount === 0 && legacyButtonContainer.childElementCount === 0) { - const toggleButton = userRow.querySelector(".dropdownToggle") as HTMLAnchorElement; - toggleButton.classList.add("disabled"); - - return; - } - - UiDropdownSimple.registerCallback(dropdownId, (identifier, action) => { - if (action === "open") { - this.rebuild(dropdownMenu, legacyButtonContainer); - } - }); - - const editLink = dropdownMenu.querySelector(".jsEditLink") as HTMLAnchorElement; - if (editLink !== null) { - const toggleButton = userRow.querySelector(".dropdownToggle") as HTMLAnchorElement; - toggleButton.addEventListener("dblclick", (event) => { - event.preventDefault(); - - editLink.click(); - }); - } - - const deleteContent = dropdownMenu.querySelector(".jsDeleteContent"); - if (deleteContent !== null) { - new AcpUserContentRemoveHandler(deleteContent as HTMLAnchorElement, userId); - } - - const sendNewPassword = dropdownMenu.querySelector(".jsSendNewPassword"); - if (sendNewPassword !== null) { - new SendNewPasswordAction(sendNewPassword as HTMLAnchorElement, userId, userRow); - } - - const toggleConfirmEmail = dropdownMenu.querySelector(".jsConfirmEmailToggle"); - if (toggleConfirmEmail !== null) { - new ToggleConfirmEmailAction(toggleConfirmEmail as HTMLAnchorElement, userId, userRow); - } - - const enableUser = dropdownMenu.querySelector(".jsEnable"); - if (enableUser !== null) { - new DisableAction(enableUser as HTMLAnchorElement, userId, userRow); - } - - const banUser = dropdownMenu.querySelector(".jsBan"); - if (banUser !== null) { - new BanAction(banUser as HTMLAnchorElement, userId, userRow); - } - - const deleteUser = dropdownMenu.querySelector(".jsDelete"); - if (deleteUser !== null) { - new DeleteAction(deleteUser as HTMLAnchorElement, userId, userRow); - } - } - - /** - * Rebuilds the dropdown by adding wrapper links for legacy buttons, - * that will eventually receive the click event. - */ - private rebuild(dropdownMenu: HTMLElement, legacyButtonContainer: HTMLElement): void { - dropdownMenu.querySelectorAll(".jsLegacyItem").forEach((element) => element.remove()); - - // inject buttons - const items: HTMLLIElement[] = []; - Array.from(legacyButtonContainer.children).forEach((button: HTMLAnchorElement) => { - const item = document.createElement("li"); - item.className = "jsLegacyItem"; - item.innerHTML = ''; - - const link = item.children[0] as HTMLAnchorElement; - link.textContent = button.dataset.tooltip || button.title; - link.addEventListener("click", (event) => { - event.preventDefault(); - - // forward click onto original button - if (button.nodeName === "A") { - button.click(); - } else { - Core.triggerEvent(button, "click"); - } - }); - - items.push(item); - }); - - items.forEach((item) => { - dropdownMenu.insertAdjacentElement("afterbegin", item); - }); - - // check if there are visible items before each divider - const listItems = Array.from(dropdownMenu.children) as HTMLElement[]; - listItems.forEach((element) => DomUtil.show(element)); - - let hasItem = false; - listItems.forEach((item) => { - if (item.classList.contains("dropdownDivider")) { - if (!hasItem) { - DomUtil.hide(item); - } - } else { - hasItem = true; - } - }); - } - - private refreshUsers(data: RefreshUsersData): void { - document.querySelectorAll(".jsUserRow").forEach((userRow: HTMLTableRowElement) => { - const userId = ~~userRow.dataset.objectId!; - if (data.userIds.includes(userId)) { - const userStatusIcons = userRow.querySelector(".userStatusIcons") as HTMLElement; - - const banned = Core.stringToBool(userRow.dataset.banned!); - let iconBanned = userRow.querySelector(".jsUserStatusBanned") as HTMLElement; - if (banned && iconBanned === null) { - iconBanned = document.createElement("span"); - iconBanned.innerHTML = ''; - iconBanned.classList.add("jsUserStatusBanned", "jsTooltip"); - iconBanned.title = getPhrase("wcf.user.status.banned"); - - userStatusIcons.appendChild(iconBanned); - } else if (!banned && iconBanned !== null) { - iconBanned.remove(); - } - - const isDisabled = !Core.stringToBool(userRow.dataset.enabled!); - let iconIsDisabled = userRow.querySelector(".jsUserStatusIsDisabled") as HTMLElement; - if (isDisabled && iconIsDisabled === null) { - iconIsDisabled = document.createElement("span"); - iconIsDisabled.innerHTML = ''; - iconIsDisabled.classList.add("jsUserStatusIsDisabled", "jsTooltip"); - iconIsDisabled.title = getPhrase("wcf.user.status.isDisabled"); - userStatusIcons.appendChild(iconIsDisabled); - } else if (!isDisabled && iconIsDisabled !== null) { - iconIsDisabled.remove(); - } - } - }); - } -} - -export = AcpUiUserEditor; diff --git a/ts/WoltLabSuite/Core/Api/Devtools/SetLanguage.ts b/ts/WoltLabSuite/Core/Api/Devtools/SetLanguage.ts new file mode 100644 index 00000000000..d84d4708780 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Devtools/SetLanguage.ts @@ -0,0 +1,22 @@ +/** + * Sets the language of the active user through the developer tools. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + * @woltlabExcludeBundle all + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { fromInfallibleApiRequest } from "../Result"; + +export async function setLanguage(languageId: number): Promise<[]> { + return fromInfallibleApiRequest(() => { + return prepareRequest(`${window.WSC_RPC_API_URL}core/devtools/set-language`) + .post({ + languageID: languageId, + }) + .fetchAsJson(); + }); +} diff --git a/ts/WoltLabSuite/Core/Api/GridViews/GetRow.ts b/ts/WoltLabSuite/Core/Api/GridViews/GetRow.ts index f1f0a4864d0..1a14ee947e7 100644 --- a/ts/WoltLabSuite/Core/Api/GridViews/GetRow.ts +++ b/ts/WoltLabSuite/Core/Api/GridViews/GetRow.ts @@ -19,6 +19,7 @@ export async function getRow( objectId: string | number, filters?: Map, gridViewParameters?: Map, + columns?: string[], ): Promise { const url = new URL(`${window.WSC_RPC_API_URL}core/grid-views/row`); url.searchParams.set("gridView", gridViewClass); @@ -39,6 +40,11 @@ export async function getRow( } }); } + if (columns) { + columns.forEach((column, index) => { + url.searchParams.set(`columns[${index}]`, column); + }); + } return fromInfallibleApiRequest(() => { return prepareRequest(url).get().allowCaching().disableLoadingIndicator().fetchAsJson(); diff --git a/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts b/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts index b45595222e2..7b7dc81efbf 100644 --- a/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts +++ b/ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts @@ -15,6 +15,7 @@ type Response = { pages: number; totalRows: number; filterLabels: ArrayLike; + header?: string; }; export async function getRows( @@ -24,6 +25,8 @@ export async function getRows( sortOrder: string = "ASC", filters?: Map, gridViewParameters?: Map, + columns?: string[], + includeHeader = false, ): Promise { const url = new URL(`${window.WSC_RPC_API_URL}core/grid-views/rows`); url.searchParams.set("gridView", gridViewClass); @@ -46,6 +49,14 @@ export async function getRows( } }); } + if (columns) { + columns.forEach((column, index) => { + url.searchParams.set(`columns[${index}]`, column); + }); + } + if (includeHeader) { + url.searchParams.set("includeHeader", "true"); + } return fromInfallibleApiRequest(() => { return prepareRequest(url).get().allowCaching().disableLoadingIndicator().fetchAsJson(); diff --git a/ts/WoltLabSuite/Core/Api/Users/GetAdminUserProfileHeader.ts b/ts/WoltLabSuite/Core/Api/Users/GetAdminUserProfileHeader.ts new file mode 100644 index 00000000000..9f929a2ea2f --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/GetAdminUserProfileHeader.ts @@ -0,0 +1,23 @@ +/** + * Gets the html code for the rendering of the user profile header in the admin panel. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { fromInfallibleApiRequest } from "WoltLabSuite/Core/Api/Result"; + +type Response = { + template: string; +}; + +export async function getAdminUserProfileHeader(userId: number): Promise { + const url = new URL(`${window.WSC_RPC_API_URL}core/users/${userId}/admin-profile-header`); + + return fromInfallibleApiRequest(() => { + return prepareRequest(url).get().fetchAsJson(); + }); +} diff --git a/ts/WoltLabSuite/Core/Api/Users/GetUserActivityPointList.ts b/ts/WoltLabSuite/Core/Api/Users/GetUserActivityPointList.ts new file mode 100644 index 00000000000..cc18c865ed3 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/GetUserActivityPointList.ts @@ -0,0 +1,23 @@ +/** + * Gets the html code for the rendering of the detailed activity point list of a user. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { fromInfallibleApiRequest } from "WoltLabSuite/Core/Api/Result"; + +type Response = { + template: string; +}; + +export async function getUserActivityPointList(userId: number): Promise { + const url = new URL(`${window.WSC_RPC_API_URL}core/users/${userId}/activity-points`); + + return fromInfallibleApiRequest(() => { + return prepareRequest(url).get().fetchAsJson(); + }); +} diff --git a/ts/WoltLabSuite/Core/Api/Users/RenderSignaturePreview.ts b/ts/WoltLabSuite/Core/Api/Users/RenderSignaturePreview.ts new file mode 100644 index 00000000000..e55ee62cf7e --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/RenderSignaturePreview.ts @@ -0,0 +1,28 @@ +/** + * Renders the preview of the active user's signature. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; + +type Response = { + message: string; +}; + +export async function renderSignaturePreview(message: string): Promise> { + const url = new URL(`${window.WSC_RPC_API_URL}core/users/signature/preview`); + + let response: Response; + try { + response = (await prepareRequest(url).post({ message }).fetchAsJson()) as Response; + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue(response); +} diff --git a/ts/WoltLabSuite/Core/Api/Users/RenderUserProfileMenuContent.ts b/ts/WoltLabSuite/Core/Api/Users/RenderUserProfileMenuContent.ts new file mode 100644 index 00000000000..5fb52e6d812 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/RenderUserProfileMenuContent.ts @@ -0,0 +1,24 @@ +/** + * Renders the content of a user profile menu item. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { fromInfallibleApiRequest } from "WoltLabSuite/Core/Api/Result"; + +type Response = { + template: string; +}; + +export async function renderUserProfileMenuContent(userId: number, menuItem: string): Promise { + const url = new URL(`${window.WSC_RPC_API_URL}core/users/${userId}/profile-menu-content`); + url.searchParams.set("menuItem", menuItem); + + return fromInfallibleApiRequest(() => { + return prepareRequest(url).get().fetchAsJson(); + }); +} diff --git a/ts/WoltLabSuite/Core/Api/Users/SaveUserConsent.ts b/ts/WoltLabSuite/Core/Api/Users/SaveUserConsent.ts new file mode 100644 index 00000000000..1f6cfda4ba7 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/SaveUserConsent.ts @@ -0,0 +1,24 @@ +/** + * Saves the consent of the active user to always display external media. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; + +export async function saveUserConsent(): Promise> { + try { + await prepareRequest(`${window.WSC_RPC_API_URL}core/users/user-consent`) + .post() + .disableLoadingIndicator() + .fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/Api/Users/ValidateEmail.ts b/ts/WoltLabSuite/Core/Api/Users/ValidateEmail.ts new file mode 100644 index 00000000000..70910538ea9 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/ValidateEmail.ts @@ -0,0 +1,28 @@ +/** + * Validates the email address for the registration process. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { fromInfallibleApiRequest } from "WoltLabSuite/Core/Api/Result"; + +type Response = + | { + ok: true; + } + | { + ok: false; + error: "invalid" | "notUnique"; + }; + +export async function validateEmail(email: string): Promise { + const url = new URL(`${window.WSC_RPC_API_URL}core/users/validate-email`); + + return fromInfallibleApiRequest(() => { + return prepareRequest(url).post({ email }).fetchAsJson(); + }); +} diff --git a/ts/WoltLabSuite/Core/Api/Users/ValidateUsername.ts b/ts/WoltLabSuite/Core/Api/Users/ValidateUsername.ts new file mode 100644 index 00000000000..1af4be50af5 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Users/ValidateUsername.ts @@ -0,0 +1,28 @@ +/** + * Validates the username for the registration process. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { fromInfallibleApiRequest } from "WoltLabSuite/Core/Api/Result"; + +type Response = + | { + ok: true; + } + | { + ok: false; + error: "invalid" | "notUnique"; + }; + +export async function validateUsername(username: string): Promise { + const url = new URL(`${window.WSC_RPC_API_URL}core/users/validate-username`); + + return fromInfallibleApiRequest(() => { + return prepareRequest(url).post({ username }).fetchAsJson(); + }); +} diff --git a/ts/WoltLabSuite/Core/Component/GridView.ts b/ts/WoltLabSuite/Core/Component/GridView.ts index 78338c938f9..44fc6b7c49d 100644 --- a/ts/WoltLabSuite/Core/Component/GridView.ts +++ b/ts/WoltLabSuite/Core/Component/GridView.ts @@ -37,6 +37,7 @@ export class GridView { defaultSortOrder = "ASC", bulkInteractionProviderClassName: string, gridViewParameters?: Map, + visibleColumns?: string[], ) { this.#gridClassName = gridClassName; this.#table = document.getElementById(`${gridId}_table`) as HTMLTableElement; @@ -45,11 +46,20 @@ export class GridView { this.#gridViewParameters = gridViewParameters; this.#initInteractions(); - this.#state = this.#setupState(gridId, pageNo, baseUrl, sortField, sortOrder, defaultSortField, defaultSortOrder); + this.#state = this.#setupState( + gridId, + pageNo, + baseUrl, + sortField, + sortOrder, + defaultSortField, + defaultSortOrder, + visibleColumns, + ); this.#initEventListeners(); } - async #loadRows(cause: StateChangeCause): Promise { + async #loadRows(cause: StateChangeCause, includeHeader = false): Promise { const response = await getRows( this.#gridClassName, this.#state.getPageNo(), @@ -57,7 +67,12 @@ export class GridView { this.#state.getSortOrder(), this.#state.getActiveFilters(), this.#gridViewParameters, + this.#state.getVisibleColumns(), + includeHeader, ); + if (response.header !== undefined) { + this.#replaceHeaderColumns(response.header); + } DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.template); this.#table.hidden = response.totalRows == 0; @@ -73,6 +88,7 @@ export class GridView { row.dataset.objectId!, this.#state.getActiveFilters(), this.#gridViewParameters, + this.#state.getVisibleColumns(), ); row.replaceWith(DomUtil.createFragmentFromHtml(template)); @@ -81,6 +97,16 @@ export class GridView { this.#checkEmptyTable(); } + #replaceHeaderColumns(html: string): void { + const headerRow = this.#table.querySelector("thead .gridView__headerRow")!; + headerRow.querySelectorAll("th[data-id]").forEach((element) => element.remove()); + + // The selection and action columns are not part of the configurable + // columns and must be preserved. + const actionColumn = headerRow.querySelector(".gridView__actionColumn"); + headerRow.insertBefore(DomUtil.createFragmentFromHtml(html), actionColumn); + } + #initInteractions(): void { wheneverFirstSeen(`#${this.#table.id} tbody tr`, (row) => { const containers = [row]; @@ -149,6 +175,7 @@ export class GridView { sortOrder: string, defaultSortField: string, defaultSortOrder: string, + visibleColumns?: string[], ): State { const state = new State( gridId, @@ -159,9 +186,10 @@ export class GridView { sortOrder, defaultSortField, defaultSortOrder, + visibleColumns, ); state.addEventListener("grid-view:change", (event) => { - void this.#loadRows(event.detail.source); + void this.#loadRows(event.detail.source, event.detail.includeHeader); }); state.addEventListener( "grid-view:get-bulk-interactions", diff --git a/ts/WoltLabSuite/Core/Component/GridView/ColumnConfiguration.ts b/ts/WoltLabSuite/Core/Component/GridView/ColumnConfiguration.ts new file mode 100644 index 00000000000..9d7093db872 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/GridView/ColumnConfiguration.ts @@ -0,0 +1,98 @@ +/** + * Handles the configuration of the visible columns of grid views. The + * configuration is not persisted, but it is part of the url. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { promiseMutex } from "../../Helper/PromiseMutex"; +import { dialogFactory } from "../Dialog"; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class ColumnConfiguration extends EventTarget { + readonly #button: HTMLButtonElement | null; + #visibleColumns?: string[]; + + constructor(gridId: string, visibleColumns?: string[]) { + super(); + + this.#visibleColumns = visibleColumns; + + this.#button = document.getElementById(`${gridId}_columnConfigurationButton`) as HTMLButtonElement | null; + this.#button?.addEventListener( + "click", + promiseMutex(() => this.#showDialog()), + ); + } + + /** + * Returns the ids of the visible columns or `undefined` if the user + * did not configure the columns. + */ + getVisibleColumns(): string[] | undefined { + return this.#visibleColumns !== undefined ? [...this.#visibleColumns] : undefined; + } + + getQueryParameters(): [string, string][] { + if (this.#visibleColumns === undefined) { + return []; + } + + return this.#visibleColumns.map((column) => ["columns[]", column]); + } + + /** + * Reads the visible columns from the given search params and returns + * true if they differ from the current configuration. + */ + updateFromSearchParams(params: URLSearchParams): boolean { + const columns: string[] = []; + params.forEach((value, key) => { + if (/^columns\[\d*\]$/.test(key)) { + columns.push(value); + } + }); + + const visibleColumns = columns.length > 0 ? columns : undefined; + const hasChanged = visibleColumns?.join(",") !== this.#visibleColumns?.join(","); + this.#visibleColumns = visibleColumns; + + return hasChanged; + } + + async #showDialog(): Promise { + const url = new URL(this.#button!.dataset.endpoint!); + this.#visibleColumns?.forEach((column, index) => { + url.searchParams.set(`columns[${index}]`, column); + }); + + const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(url.toString()); + + if (ok) { + // `null` represents the default configuration. + this.#visibleColumns = result ?? undefined; + + this.dispatchEvent(new CustomEvent("grid-view:change")); + } + } +} + +interface ColumnConfigurationEventMap { + "grid-view:change": CustomEvent; +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface ColumnConfiguration extends EventTarget { + addEventListener: { + ( + type: T, + listener: (this: ColumnConfiguration, ev: ColumnConfigurationEventMap[T]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + } & HTMLElement["addEventListener"]; +} + +export default ColumnConfiguration; diff --git a/ts/WoltLabSuite/Core/Component/GridView/Sorting.ts b/ts/WoltLabSuite/Core/Component/GridView/Sorting.ts index b3ec2b67ec5..ee2f094b003 100644 --- a/ts/WoltLabSuite/Core/Component/GridView/Sorting.ts +++ b/ts/WoltLabSuite/Core/Component/GridView/Sorting.ts @@ -30,14 +30,17 @@ export class Sorting extends EventTarget { this.#defaultSortOrder = defaultSortOrder; this.#table = table; - this.#table - .querySelectorAll('.gridView__headerColumn[data-sortable="1"]') - .forEach((element) => { - const button = element.querySelector(".gridView__headerColumn__button"); - button?.addEventListener("click", () => { - this.#sort(element.dataset.id!, element.dataset.defaultSortOrder ?? "ASC"); - }); - }); + // The header columns can be replaced at runtime when the user configures + // the visible columns, therefore the click events are delegated. + this.#table.tHead?.addEventListener("click", (event) => { + const button = (event.target as HTMLElement).closest(".gridView__headerColumn__button"); + const element = button?.closest('.gridView__headerColumn[data-sortable="1"]'); + if (!element) { + return; + } + + this.#sort(element.dataset.id!, element.dataset.defaultSortOrder ?? "ASC"); + }); this.#renderActiveSorting(); } diff --git a/ts/WoltLabSuite/Core/Component/GridView/State.ts b/ts/WoltLabSuite/Core/Component/GridView/State.ts index 19d6c7f26cb..524b878120a 100644 --- a/ts/WoltLabSuite/Core/Component/GridView/State.ts +++ b/ts/WoltLabSuite/Core/Component/GridView/State.ts @@ -7,6 +7,7 @@ * @since 6.2 */ +import ColumnConfiguration from "./ColumnConfiguration"; import Filter from "./Filter"; import Selection from "./Selection"; import Sorting from "./Sorting"; @@ -20,6 +21,7 @@ export const enum StateChangeCause { // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class State extends EventTarget { readonly #baseUrl: string; + readonly #columnConfiguration: ColumnConfiguration; readonly #filter: Filter; readonly #pagination: WoltlabCorePaginationElement; readonly #selection: Selection; @@ -37,6 +39,7 @@ export class State extends EventTarget { sortOrder: string, defaultSortField: string, defaultSortOrder: string, + visibleColumns?: string[], ) { super(); @@ -55,6 +58,14 @@ export class State extends EventTarget { this.#switchPage(1, StateChangeCause.Change); }); + this.#columnConfiguration = new ColumnConfiguration(gridId, visibleColumns); + this.#columnConfiguration.addEventListener("grid-view:change", () => { + // Changing the visible columns does not affect the current page. + this.dispatchEvent( + new CustomEvent("grid-view:change", { detail: { source: StateChangeCause.Change, includeHeader: true } }), + ); + }); + this.#sorting = new Sorting(table, sortField, sortOrder, defaultSortField, defaultSortOrder); this.#sorting.addEventListener("grid-view:change", () => { this.#switchPage(1, StateChangeCause.Change); @@ -97,6 +108,10 @@ export class State extends EventTarget { return this.#filter.getActiveFilters(); } + getVisibleColumns(): string[] | undefined { + return this.#columnConfiguration.getVisibleColumns(); + } + getSelectedIds(): number[] { return this.#selection.getSelectedIds(); } @@ -115,11 +130,11 @@ export class State extends EventTarget { this.#updateGridViewFooter(); } - #switchPage(pageNo: number, source: StateChangeCause): void { + #switchPage(pageNo: number, source: StateChangeCause, includeHeader = false): void { this.#pagination.page = pageNo; this.#pageNo = pageNo; - this.dispatchEvent(new CustomEvent("grid-view:change", { detail: { source } })); + this.dispatchEvent(new CustomEvent("grid-view:change", { detail: { source, includeHeader } })); } #updateQueryString(): void { @@ -156,6 +171,10 @@ export class State extends EventTarget { parameters.push(parameter); } + for (const parameter of this.#columnConfiguration.getQueryParameters()) { + parameters.push(parameter); + } + if (parameters.length > 0) { url.search += url.search !== "" ? "&" : "?"; url.search += new URLSearchParams(parameters).toString(); @@ -183,8 +202,9 @@ export class State extends EventTarget { this.#filter.updateFromSearchParams(url.searchParams); this.#sorting.updateFromSearchParams(url.searchParams); + const columnsChanged = this.#columnConfiguration.updateFromSearchParams(url.searchParams); - this.#switchPage(pageNo, StateChangeCause.History); + this.#switchPage(pageNo, StateChangeCause.History, columnsChanged); } #updateLastUrl(): void { @@ -228,7 +248,7 @@ export class State extends EventTarget { } interface StateEventMap { - "grid-view:change": CustomEvent<{ source: StateChangeCause }>; + "grid-view:change": CustomEvent<{ source: StateChangeCause; includeHeader: boolean }>; "grid-view:get-bulk-interactions": CustomEvent<{ objectIds: number[] }>; } diff --git a/ts/WoltLabSuite/Core/Component/LanguageSwitcher.ts b/ts/WoltLabSuite/Core/Component/LanguageSwitcher.ts index 57848e89c65..3ee940bd4b0 100644 --- a/ts/WoltLabSuite/Core/Component/LanguageSwitcher.ts +++ b/ts/WoltLabSuite/Core/Component/LanguageSwitcher.ts @@ -8,7 +8,6 @@ * @since 6.3 */ -import { dboAction } from "../Ajax"; import User from "../User"; function switchLanguage(languageId: string, languageCode: string): void { @@ -50,9 +49,8 @@ async function switchLanguageDevtools(languageId: string, languageCode: string): } } - await dboAction("devtoolsSetLanguage", "wcf\\data\\user\\UserAction") - .payload({ languageID: parseInt(languageId, 10) }) - .dispatch(); + const { setLanguage } = await import("../Api/Devtools/SetLanguage"); + await setLanguage(parseInt(languageId, 10)); window.location.reload(); } diff --git a/ts/WoltLabSuite/Core/Component/User/ActivityPointList.ts b/ts/WoltLabSuite/Core/Component/User/ActivityPointList.ts index 886c55914a2..7ab67f59626 100644 --- a/ts/WoltLabSuite/Core/Component/User/ActivityPointList.ts +++ b/ts/WoltLabSuite/Core/Component/User/ActivityPointList.ts @@ -7,22 +7,16 @@ * @since 6.2 */ -import { dboAction } from "WoltLabSuite/Core/Ajax"; +import { getUserActivityPointList } from "WoltLabSuite/Core/Api/Users/GetUserActivityPointList"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; import { getPhrase } from "WoltLabSuite/Core/Language"; -type ResponseGetDetailedActivityPointList = { - template: string; -}; - async function showDialog(userId: number): Promise { - const response = (await dboAction("getDetailedActivityPointList", "wcf\\data\\user\\UserProfileAction") - .objectIds([userId]) - .dispatch()) as ResponseGetDetailedActivityPointList; + const { template } = await getUserActivityPointList(userId); - const dialog = dialogFactory().fromHtml(response.template).withoutControls(); + const dialog = dialogFactory().fromHtml(template).withoutControls(); dialog.show(getPhrase("wcf.user.activityPoint")); } diff --git a/ts/WoltLabSuite/Core/Component/User/Avatar.ts b/ts/WoltLabSuite/Core/Component/User/Avatar.ts index f63a7734e1f..d4200d2273c 100644 --- a/ts/WoltLabSuite/Core/Component/User/Avatar.ts +++ b/ts/WoltLabSuite/Core/Component/User/Avatar.ts @@ -49,8 +49,7 @@ export function setup(): void { }, ); - const avatarForm = document.getElementById("avatarForm"); - if (avatarForm) { + if (document.body.classList.contains("wcfAcp")) { registerCallback("wcf\\action\\UserAvatarAction_avatarFileID", (fileId: number | undefined) => { let link = defaultAvatar; if (fileId !== undefined) { @@ -61,7 +60,10 @@ export function setup(): void { link = file.link!; } - avatarForm.querySelector("img.userAvatarImage")!.src = link; + const headerAvatar = document.querySelector(".userProfileHeader__avatar img.userAvatarImage"); + if (headerAvatar) { + headerAvatar.src = link; + } document .getElementById("wcf\\action\\UserAvatarAction_avatarFileIDContainer") diff --git a/ts/WoltLabSuite/Core/Component/User/CoverPhoto.ts b/ts/WoltLabSuite/Core/Component/User/CoverPhoto.ts index 4acac75c5da..cb0b0bc4a99 100644 --- a/ts/WoltLabSuite/Core/Component/User/CoverPhoto.ts +++ b/ts/WoltLabSuite/Core/Component/User/CoverPhoto.ts @@ -14,9 +14,6 @@ import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; import * as FormBuilderManager from "WoltLabSuite/Core/Form/Builder/Manager"; import { fire as fireEvent } from "WoltLabSuite/Core/Event/Handler"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import DomUtil from "WoltLabSuite/Core/Dom/Util"; -import { escapeHTML } from "WoltLabSuite/Core/StringUtil"; import { registerCallback } from "WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor"; import WoltlabCoreFile from "WoltLabSuite/Core/Component/File/woltlab-core-file"; import { showDefaultSuccessSnackbar } from "../Snackbar"; @@ -40,69 +37,41 @@ async function editCoverPhoto(button: HTMLElement): Promise { dialog.show(json.title); } -function getCoverPhotoElement(): HTMLElement | null { - return ( - document.querySelector(".userProfileHeader__coverPhotoImage") ?? - document.getElementById("coverPhotoPreview") - ); +function getDefaultCoverPhoto(): string { + return document.querySelector("[data-default-cover-photo]")?.dataset.defaultCoverPhoto ?? ""; } export function setup(): void { - wheneverFirstSeen("[data-edit-cover-photo]", (button) => { - const defaultCoverPhoto = button.dataset.defaultCoverPhoto; - - registerCallback("wcf\\action\\UserCoverPhotoAction_coverPhotoFileID", (fileId: number | undefined) => { - const coverPhotoElement = getCoverPhotoElement(); + registerCallback("wcf\\action\\UserCoverPhotoAction_coverPhotoFileID", (fileId: number | undefined) => { + const headerImage = document.querySelector(".userProfileHeader__coverPhotoImage"); - if (coverPhotoElement && parseInt(coverPhotoElement.dataset.objectId!) === fileId) { - // nothing changed - return; - } - - const file = document.querySelector( - `#wcf\\\\action\\\\UserCoverPhotoAction_coverPhotoFileIDContainer woltlab-core-file[file-id="${fileId}"]`, - ); - const coverPhotoNotice = document.getElementById("coverPhotoNotice"); - const coverPhotoUrl = file?.link ?? defaultCoverPhoto ?? ""; - const coverPhotoStyle = `url("${coverPhotoUrl}")`; + if (headerImage && parseInt(headerImage.dataset.objectId!) === fileId) { + // nothing changed + return; + } - if (coverPhotoElement instanceof HTMLImageElement && coverPhotoUrl) { - coverPhotoElement.src = coverPhotoUrl; + const file = document.querySelector( + `#wcf\\\\action\\\\UserCoverPhotoAction_coverPhotoFileIDContainer woltlab-core-file[file-id="${fileId}"]`, + ); + const coverPhotoUrl = file?.link || getDefaultCoverPhoto(); - coverPhotoElement.dataset.objectId = fileId?.toString() || ""; + if (headerImage && coverPhotoUrl) { + headerImage.src = coverPhotoUrl; + headerImage.dataset.objectId = fileId?.toString() || ""; - document - .getElementById("wcf\\action\\UserCoverPhotoAction_coverPhotoFileIDContainer") - ?.closest("woltlab-core-dialog") - ?.close(); - } else { - // ACP cover photo management - if (!coverPhotoElement && coverPhotoUrl) { - coverPhotoNotice!.parentElement!.appendChild( - DomUtil.createFragmentFromHtml( - `
`, - ), - ); - coverPhotoNotice!.remove(); - } else if (coverPhotoElement && !coverPhotoUrl) { - coverPhotoElement.parentElement!.appendChild( - DomUtil.createFragmentFromHtml( - `${getPhrase("wcf.user.coverPhoto.noImage")}`, - ), - ); - coverPhotoElement.remove(); - } else if (coverPhotoElement && coverPhotoUrl) { - coverPhotoElement.style.backgroundImage = coverPhotoStyle; - coverPhotoElement.dataset.objectId = fileId?.toString() || ""; - } - } + document + .getElementById("wcf\\action\\UserCoverPhotoAction_coverPhotoFileIDContainer") + ?.closest("woltlab-core-dialog") + ?.close(); + } - showDefaultSuccessSnackbar(); - fireEvent("com.woltlab.wcf.user", "coverPhoto", { - url: coverPhotoUrl, - }); + showDefaultSuccessSnackbar(); + fireEvent("com.woltlab.wcf.user", "coverPhoto", { + url: coverPhotoUrl, }); + }); + wheneverFirstSeen("[data-edit-cover-photo]", (button) => { button.addEventListener( "click", promiseMutex(() => editCoverPhoto(button)), diff --git a/ts/WoltLabSuite/Core/Component/User/Profile/Editor.ts b/ts/WoltLabSuite/Core/Component/User/Profile/Editor.ts new file mode 100644 index 00000000000..37b91c08f49 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/User/Profile/Editor.ts @@ -0,0 +1,34 @@ +/** + * Edits the profile of a user in a dialog. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; +import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; + +async function editProfile(endpoint: string, onSuccess: () => Promise): Promise { + const { ok } = await dialogFactory().usingFormBuilder().fromEndpoint(endpoint); + if (!ok) { + return; + } + + await onSuccess(); + + showDefaultSuccessSnackbar(); +} + +export function setup(button: HTMLElement, editOnInit: boolean, onSuccess: () => Promise): void { + const endpoint = button.dataset.endpoint!; + + const openDialog = promiseMutex(() => editProfile(endpoint, onSuccess)); + button.addEventListener("click", () => openDialog()); + + if (editOnInit) { + openDialog(); + } +} diff --git a/ts/WoltLabSuite/Core/Component/User/Signature/Preview.ts b/ts/WoltLabSuite/Core/Component/User/Signature/Preview.ts index a30e18b2bac..f266582847c 100644 --- a/ts/WoltLabSuite/Core/Component/User/Signature/Preview.ts +++ b/ts/WoltLabSuite/Core/Component/User/Signature/Preview.ts @@ -8,24 +8,26 @@ */ import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; -import { dboAction } from "WoltLabSuite/Core/Ajax"; +import { renderSignaturePreview } from "WoltLabSuite/Core/Api/Users/RenderSignaturePreview"; import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; - -type ResponseGetMessagePreview = { - message: string; - raw: string; -}; +import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; +import { innerError } from "WoltLabSuite/Core/Dom/Util"; let previewContainer: HTMLElement; -async function loadPreview(message: string): Promise { - const response = (await dboAction("getMessagePreview", "wcf\\data\\user\\UserProfileAction") - .payload({ - data: { - message, - }, - }) - .dispatch()) as ResponseGetMessagePreview; +async function loadPreview(ckeditor: CKEditor): Promise { + const response = await renderSignaturePreview(ckeditor.getHtml()); + if (!response.ok) { + const validationError = response.error.getValidationError(); + if (validationError === undefined) { + throw new Error("Unexpected validation error", { cause: response.error }); + } + + innerError(ckeditor.element, validationError.code); + return; + } + + innerError(ckeditor.element, false); if (previewContainer === undefined) { const template = document.getElementById("previewTemplate") as HTMLTemplateElement; @@ -36,14 +38,14 @@ async function loadPreview(message: string): Promise { previewContainer = document.getElementById("previewContainer")!; } - previewContainer.innerHTML = response.message; + previewContainer.innerHTML = response.value.message; } export function setup(): void { listenToCkeditor(document.getElementById("text")!).ready(({ ckeditor }) => { document.getElementById("previewButton")?.addEventListener( "click", - promiseMutex(() => loadPreview(ckeditor.getHtml())), + promiseMutex(() => loadPreview(ckeditor)), ); }); } diff --git a/ts/WoltLabSuite/Core/Controller/User/Profile.ts b/ts/WoltLabSuite/Core/Controller/User/Profile.ts index 2c0fe49ab08..51c8bad5744 100644 --- a/ts/WoltLabSuite/Core/Controller/User/Profile.ts +++ b/ts/WoltLabSuite/Core/Controller/User/Profile.ts @@ -6,13 +6,14 @@ * @license GNU Lesser General Public License */ -import { dboAction } from "WoltLabSuite/Core/Ajax"; import * as EventHandler from "WoltLabSuite/Core/Event/Handler"; import { insertHtml } from "WoltLabSuite/Core/Dom/Util"; import { trigger as triggerDomChange } from "WoltLabSuite/Core/Dom/Change/Listener"; import { getTabMenu } from "WoltLabSuite/Core/Ui/TabMenu"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; +import { renderUserProfileMenuContent } from "WoltLabSuite/Core/Api/Users/RenderUserProfileMenuContent"; +import { setup as setupProfileEditor } from "WoltLabSuite/Core/Component/User/Profile/Editor"; function setupUserList(userId: number, buttonId: string, className: string): void { const button = document.getElementById(buttonId) as HTMLElement; @@ -62,23 +63,12 @@ function setupTabMenu(userId: number): void { }); } -type ResponseGetTabContent = { - template: string; -}; - async function loadTabMenuContent(userId: number, tabName: string): Promise { if (tabContentLoaded.has(tabName)) { return; } - const response = (await dboAction("getContent", "wcf\\data\\user\\profile\\menu\\item\\UserProfileMenuItemAction") - .payload({ - data: { - menuItem: tabName, - userID: userId, - }, - }) - .dispatch()) as ResponseGetTabContent; + const response = await renderUserProfileMenuContent(userId, tabName); tabContentLoaded.set(tabName, true); @@ -86,9 +76,39 @@ async function loadTabMenuContent(userId: number, tabName: string): Promise { + const container = document.querySelector('.tabMenuContent[data-name="' + tabName + '"]'); + if (container === null) { + return; + } + + tabContentLoaded.delete(tabName); + container.innerHTML = ""; + + await loadTabMenuContent(userId, tabName); +} + +function setupEditor(userId: number, editOnInit: boolean): void { + const button = document.querySelector(".jsButtonEditProfile"); + if (button === null) { + return; + } + + setupProfileEditor(button, editOnInit, async () => { + // Reload the tab before selecting it, otherwise the select event would load + // the content of a tab that has not been loaded yet a second time. + await reloadTabMenuContent(userId, "about"); + getTabMenu("profileContent")?.select("about"); + }); +} + +export function setup(userId: number, editOnInit: boolean = false): void { setupFollowingList(userId); setupFollowerList(userId); setupVisitorList(userId); setupTabMenu(userId); + setupEditor(userId, editOnInit); } diff --git a/ts/WoltLabSuite/Core/Controller/User/Registration.ts b/ts/WoltLabSuite/Core/Controller/User/Registration.ts index 8ad0889754a..d48c017ca03 100644 --- a/ts/WoltLabSuite/Core/Controller/User/Registration.ts +++ b/ts/WoltLabSuite/Core/Controller/User/Registration.ts @@ -6,19 +6,11 @@ * @license GNU Lesser General Public License */ -import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { validateEmail as validateEmailAddress } from "WoltLabSuite/Core/Api/Users/ValidateEmail"; +import { validateUsername as validateUsernameValue } from "WoltLabSuite/Core/Api/Users/ValidateUsername"; import { getPhrase } from "WoltLabSuite/Core/Language"; import DomUtil from "../../Dom/Util"; -type ValidationResult = - | { - ok: true; - } - | { - ok: false; - error: string; - }; - type UsernameOptions = { minlength: number; maxlength: number; @@ -37,11 +29,7 @@ async function validateUsername(username: HTMLInputElement, options: UsernameOpt return; } - const result = (await prepareRequest(username.dataset.validationEndpoint!) - .post({ - username: value, - }) - .fetchAsJson()) as ValidationResult; + const result = await validateUsernameValue(value); if (!result.ok) { showErrorMessage(username, `wcf.user.username.error.${result.error}`); return; @@ -58,11 +46,7 @@ async function validateEmail(email: HTMLInputElement): Promise { return; } - const result = (await prepareRequest(email.dataset.validationEndpoint!) - .post({ - email: value, - }) - .fetchAsJson()) as ValidationResult; + const result = await validateEmailAddress(value); if (!result.ok) { showErrorMessage(email, `wcf.user.email.error.${result.error}`); return; diff --git a/ts/WoltLabSuite/Core/Ui/Message/UserConsent.ts b/ts/WoltLabSuite/Core/Ui/Message/UserConsent.ts index 12acdd8b4e3..0ed49f3b673 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/UserConsent.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/UserConsent.ts @@ -7,7 +7,7 @@ */ import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; -import * as Ajax from "../../Ajax"; +import { saveUserConsent } from "../../Api/Users/SaveUserConsent"; import * as Core from "../../Core"; import DomChangeListener from "../../Dom/Change/Listener"; import DomUtil from "../../Dom/Util"; @@ -51,13 +51,8 @@ class UserConsent { this.enableAllExternalMedia(); if (User.userId) { - Ajax.apiOnce({ - data: { - actionName: "saveUserConsent", - className: "wcf\\data\\user\\UserAction", - }, - silent: true, - }); + // The consent is only a convenience, errors are silently ignored. + void saveUserConsent(); } else { window.sessionStorage.setItem(`${Core.getStoragePrefix()}user-consent`, "all"); } diff --git a/ts/WoltLabSuite/Core/Ui/User/Editor.ts b/ts/WoltLabSuite/Core/Ui/User/Editor.ts deleted file mode 100644 index cf841172785..00000000000 --- a/ts/WoltLabSuite/Core/Ui/User/Editor.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Simple notification overlay. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @woltlabExcludeBundle all - */ - -import * as Ajax from "../../Ajax"; -import { AjaxCallbackObject, AjaxCallbackSetup } from "../../Ajax/Data"; -import * as Core from "../../Core"; -import { DialogCallbackObject, DialogCallbackSetup } from "../Dialog/Data"; -import DomUtil from "../../Dom/Util"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import * as StringUtil from "../../StringUtil"; -import UiDialog from "../Dialog"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; - -class UserEditor implements AjaxCallbackObject, DialogCallbackObject { - private actionName = ""; - private readonly header: HTMLElement; - - constructor() { - this.header = document.querySelector(".userProfileHeader") as HTMLElement; - - ["ban", "disableAvatar", "disableCoverPhoto", "disableSignature", "enable"].forEach((action) => { - const button = document.querySelector( - ".userProfileHeader__managementOptions .jsButtonUser" + StringUtil.ucfirst(action), - ) as HTMLElement; - - // The button is missing if the current user lacks the permission. - if (button) { - button.dataset.action = action; - button.addEventListener("click", (ev) => this._click(ev)); - } - }); - } - - /** - * Handles clicks on action buttons. - */ - _click(event: MouseEvent): void { - event.preventDefault(); - - const target = event.currentTarget as HTMLElement; - const action = target.dataset.action || ""; - let actionName = ""; - switch (action) { - case "ban": - if (Core.stringToBool(this.header.dataset.banned || "")) { - actionName = "unban"; - } - break; - - case "disableAvatar": - if (Core.stringToBool(this.header.dataset.disableAvatar || "")) { - actionName = "enableAvatar"; - } - break; - - case "disableCoverPhoto": - if (Core.stringToBool(this.header.dataset.disableCoverPhoto || "")) { - actionName = "enableCoverPhoto"; - } - break; - - case "disableSignature": - if (Core.stringToBool(this.header.dataset.disableSignature || "")) { - actionName = "enableSignature"; - } - break; - - case "enable": - actionName = Core.stringToBool(this.header.dataset.isDisabled || "") ? "enable" : "disable"; - break; - } - - if (actionName === "") { - this.actionName = action; - - UiDialog.open(this); - } else { - Ajax.api(this, { - actionName: actionName, - }); - } - } - - /** - * Handles form submit and input validation. - */ - _submit(event: Event): void { - event.preventDefault(); - - const label = document.getElementById("wcfUiUserEditorExpiresLabel") as HTMLElement; - - let expires = ""; - let errorMessage = ""; - const neverExpires = document.getElementById("wcfUiUserEditorNeverExpires") as HTMLInputElement; - if (!neverExpires.checked) { - const expireValue = document.getElementById("wcfUiUserEditorExpiresDatePicker") as HTMLInputElement; - expires = expireValue.value; - if (expires === "") { - errorMessage = getPhrase("wcf.global.form.error.empty"); - } - } - - DomUtil.innerError(label, errorMessage); - - const parameters = {}; - parameters[this.actionName + "Expires"] = expires; - const reason = document.getElementById("wcfUiUserEditorReason") as HTMLTextAreaElement; - parameters[this.actionName + "Reason"] = reason.value.trim(); - - Ajax.api(this, { - actionName: this.actionName, - parameters: parameters, - }); - } - - _ajaxSuccess(data): void { - let button: HTMLElement; - switch (data.actionName) { - case "ban": - case "unban": { - this.header.dataset.banned = data.actionName === "ban" ? "true" : "false"; - button = document.querySelector(".userProfileHeader__managementOptions .jsButtonUserBan") as HTMLElement; - button.textContent = getPhrase("wcf.user." + (data.actionName === "ban" ? "unban" : "ban")); - - const contentTitle = this.header.querySelector(".userProfileHeader__username") as HTMLElement; - let banIcon = contentTitle.querySelector(".jsUserBanned") as HTMLElement; - if (data.actionName === "ban") { - banIcon = document.createElement("span"); - banIcon.innerHTML = ''; - banIcon.classList.add("jsUserBanned", "jsTooltip"); - banIcon.title = data.returnValues; - contentTitle.appendChild(banIcon); - } else if (banIcon) { - banIcon.remove(); - } - break; - } - - case "disableAvatar": - case "enableAvatar": - this.header.dataset.disableAvatar = data.actionName === "disableAvatar" ? "true" : "false"; - button = document.querySelector( - ".userProfileHeader__managementOptions .jsButtonUserDisableAvatar", - ) as HTMLElement; - button.textContent = getPhrase( - "wcf.user." + (data.actionName === "disableAvatar" ? "enable" : "disable") + "Avatar", - ); - break; - - case "disableCoverPhoto": - case "enableCoverPhoto": - this.header.dataset.disableCoverPhoto = data.actionName === "disableCoverPhoto" ? "true" : "false"; - button = document.querySelector( - ".userProfileHeader__managementOptions .jsButtonUserDisableCoverPhoto", - ) as HTMLElement; - button.textContent = getPhrase( - "wcf.user." + (data.actionName === "disableCoverPhoto" ? "enable" : "disable") + "CoverPhoto", - ); - break; - - case "disableSignature": - case "enableSignature": - this.header.dataset.disableSignature = data.actionName === "disableSignature" ? "true" : "false"; - button = document.querySelector( - ".userProfileHeader__managementOptions .jsButtonUserDisableSignature", - ) as HTMLElement; - button.textContent = getPhrase( - "wcf.user." + (data.actionName === "disableSignature" ? "enable" : "disable") + "Signature", - ); - break; - - case "enable": - case "disable": - this.header.dataset.isDisabled = data.actionName === "disable" ? "true" : "false"; - button = document.querySelector(".userProfileHeader__managementOptions .jsButtonUserEnable") as HTMLElement; - button.textContent = getPhrase("wcf.acp.user." + (data.actionName === "enable" ? "disable" : "enable")); - break; - } - - if (["ban", "disableAvatar", "disableCoverPhoto", "disableSignature"].indexOf(data.actionName) !== -1) { - UiDialog.close(this); - } - - showDefaultSuccessSnackbar(); - } - - _ajaxSetup(): ReturnType { - return { - data: { - className: "wcf\\data\\user\\UserAction", - objectIDs: [+this.header.dataset.objectId!], - }, - }; - } - - _dialogSetup(): ReturnType { - return { - id: "wcfUiUserEditor", - options: { - onSetup: (content) => { - const checkbox = document.getElementById("wcfUiUserEditorNeverExpires") as HTMLInputElement; - checkbox.addEventListener("change", () => { - const settings = document.getElementById("wcfUiUserEditorExpiresSettings") as HTMLElement; - DomUtil[checkbox.checked ? "hide" : "show"](settings); - }); - - const submitButton = content.querySelector("button.buttonPrimary") as HTMLButtonElement; - submitButton.addEventListener("click", this._submit.bind(this)); - }, - onShow: (content) => { - UiDialog.setTitle("wcfUiUserEditor", getPhrase("wcf.user." + this.actionName + ".confirmMessage")); - - const reason = document.getElementById("wcfUiUserEditorReason") as HTMLElement; - let label = reason.nextElementSibling as HTMLElement; - const phrase = "wcf.user." + this.actionName + ".reason.description"; - label.textContent = getPhrase(phrase); - if (label.textContent === phrase) { - DomUtil.hide(label); - } else { - DomUtil.show(label); - } - - label = document.getElementById("wcfUiUserEditorNeverExpires")!.nextElementSibling as HTMLElement; - label.textContent = getPhrase("wcf.user." + this.actionName + ".neverExpires"); - - label = content.querySelector('label[for="wcfUiUserEditorExpires"]') as HTMLElement; - label.textContent = getPhrase("wcf.user." + this.actionName + ".expires"); - - label = document.getElementById("wcfUiUserEditorExpiresLabel") as HTMLElement; - label.textContent = getPhrase("wcf.user." + this.actionName + ".expires.description"); - }, - }, - source: `
-
-
-
-
-
-
-
-
- -
-
- -
`, - }; - } -} - -/** - * Initializes the user editor. - */ -export function init(): void { - new UserEditor(); -} 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 c089e60b350..30e9039eb29 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 @@ -67,6 +67,12 @@ PartialDatabaseTable::create('wcf1_user_option') ->columns([ DefaultFalseBooleanDatabaseTableColumn::create('showOnUserCard'), + MediumtextDatabaseTableColumn::create('configuration'), + NotNullVarchar255DatabaseTableColumn::create('outputClass') + ->defaultValue('') + ->drop(), + MediumtextDatabaseTableColumn::create('enableOptions') + ->drop(), VarcharDatabaseTableColumn::create('l10nIdentifier') ->length(255), ]), @@ -99,6 +105,39 @@ ->onDelete('CASCADE') ->onUpdate('NO ACTION'), ]), + PartialDatabaseTable::create('wcf1_user_option_category') + ->columns([ + VarcharDatabaseTableColumn::create('l10nIdentifier') + ->length(255), + ]), + DatabaseTable::create('wcf1_user_option_category_l10n') + ->columns([ + NotNullInt10DatabaseTableColumn::create('categoryID'), + IntDatabaseTableColumn::create('languageID'), + VarcharDatabaseTableColumn::create('title') + ->length(255), + TinyintDatabaseTableColumn::create('isPristine') + ->notNull() + ->defaultValue(1), + ]) + ->indices([ + DatabaseTableIndex::create('categoryID') + ->columns(['categoryID', 'languageID']), + ]) + ->foreignKeys([ + DatabaseTableForeignKey::create() + ->columns(['categoryID']) + ->referencedTable('wcf1_user_option_category') + ->referencedColumns(['categoryID']) + ->onDelete('CASCADE') + ->onUpdate('NO ACTION'), + DatabaseTableForeignKey::create() + ->columns(['languageID']) + ->referencedTable('wcf1_language') + ->referencedColumns(['languageID']) + ->onDelete('CASCADE') + ->onUpdate('NO ACTION'), + ]), PartialDatabaseTable::create('wcf1_smiley') ->columns([ NotNullVarchar255DatabaseTableColumn::create('emoji') diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step3.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step3.php index 57ba9886576..bcb448f6a4d 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step3.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step3.php @@ -67,4 +67,12 @@ MediumtextDatabaseTableColumn::create('answers') ->drop(), ]), + // The link pattern is migrated to the configuration of the user options. + PartialDatabaseTable::create('wcf1_user_option') + ->columns([ + MediumtextDatabaseTableColumn::create('labeledUrl') + ->drop(), + MediumtextDatabaseTableColumn::create('additionalData') + ->drop(), + ]), ]; diff --git a/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php b/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php index 30135defc23..e890953b50e 100644 --- a/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php +++ b/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php @@ -1,12 +1,12 @@ executeAction(); -$action = new UserProfileAction([$editor], 'updateUserOnlineMarking'); -$action->executeAction(); +(new RefreshUserRanks([WCF::getUser()]))(); +(new RefreshUserOnlineMarkings([WCF::getUser()]))(); // install default reactions foreach ([ diff --git a/wcfsetup/install/files/acp/style/layout.scss b/wcfsetup/install/files/acp/style/layout.scss index ede9f81303e..85835cd1d6a 100644 --- a/wcfsetup/install/files/acp/style/layout.scss +++ b/wcfsetup/install/files/acp/style/layout.scss @@ -581,21 +581,6 @@ html[data-color-scheme="dark"] { } } -#coverPhotoPreview { - background: no-repeat center center; - background-size: cover; - height: 200px; - margin-bottom: 5px; - - @include screen-xs { - height: 150px; - } -} - -#uploadCoverPhoto > .button { - vertical-align: top; -} - @include screen-md-down { .pageHeaderSearch { display: block; @@ -1098,3 +1083,14 @@ html[data-color-scheme="dark"] { text-decoration: underline; } } + +/* user profile header on user edit page */ +.contentHeader + .userProfileHeader { + margin-top: 20px; +} + +.userProfileHeader__coverPhoto { + @include screen-md-up { + height: 150px; + } +} diff --git a/wcfsetup/install/files/acp/templates/__devtoolsLanguageChooser.tpl b/wcfsetup/install/files/acp/templates/__devtoolsLanguageChooser.tpl index 12d391c29f9..3b6d6e9af4f 100644 --- a/wcfsetup/install/files/acp/templates/__devtoolsLanguageChooser.tpl +++ b/wcfsetup/install/files/acp/templates/__devtoolsLanguageChooser.tpl @@ -1,6 +1,6 @@ {if ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS && !$__wcf->user->isGuest() && $__isLogin|empty} - {/if} - {/hascontent} - -
  • {icon name='list'} {lang}wcf.acp.menu.link.user.list{/lang}
  • - - {event name='contentHeaderNavigation'} - - -
    - -{if $userID|isset && $__wcf->user->userID == $userID} - {lang}wcf.acp.user.edit.warning.selfEdit{/lang} -{/if} - -{include file='shared_formNotice'} - -
    -
    -
    - -
    - - {csrfToken} -
    -
    - -{if $action === 'edit' && $ownerGroupID} - + {if $userID == $__wcf->user->userID} + {lang}wcf.acp.user.edit.warning.selfEdit{/lang} + {/if} {/if} +{unsafe:$form->getHtml()} + {include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/userAssignToGroup.tpl b/wcfsetup/install/files/acp/templates/userAssignToGroup.tpl deleted file mode 100644 index 764b62c1b60..00000000000 --- a/wcfsetup/install/files/acp/templates/userAssignToGroup.tpl +++ /dev/null @@ -1,55 +0,0 @@ -{include file='header' pageTitle='wcf.acp.user.assignToGroup'} - -
    -
    -

    {lang}wcf.acp.user.assignToGroup{/lang}

    -
    - - {hascontent} - - {/hascontent} -
    - -{include file='shared_formError'} - -
    -
    -

    {lang}wcf.acp.user.assignToGroup.markedUsers{/lang}

    - -
    - {implode from=$users item=$user}{$user}{/implode} -
    - - {event name='markedUserFields'} -
    - -
    -

    {lang}wcf.acp.user.groups{/lang}

    - - -
    - {htmlCheckboxes options=$groups name=groupIDs selected=$groupIDs} - {if $errorField == 'groupIDs'} - - {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if} - - {/if} -
    - - - {event name='userGroupFields'} -
    - - {event name='sections'} - -
    - - {csrfToken} -
    -
    - -{include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/userContentRevertChanges.tpl b/wcfsetup/install/files/acp/templates/userContentRevertChanges.tpl deleted file mode 100644 index 619ae8c1c03..00000000000 --- a/wcfsetup/install/files/acp/templates/userContentRevertChanges.tpl +++ /dev/null @@ -1,58 +0,0 @@ -{include file='header' pageTitle='wcf.acp.user.revertChanges'} - -
    -
    -

    {lang}wcf.acp.user.revertChanges{/lang}

    -
    - - {hascontent} - - {/hascontent} -
    - -{include file='shared_formError'} - -
    -
    -

    {lang}wcf.acp.user.revertChanges.markedUsers{/lang}

    - -
    - {implode from=$users item='user'}{$user}{/implode} -
    - - {event name='markedUserFields'} -
    - -
    -

    {lang}wcf.acp.user.revertChanges.timeframe{/lang}

    - - -
    -
    - - - {if $errorField == 'timeframe'} - - {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if} - - {/if} - {lang}wcf.acp.user.revertChanges.timeframe.description{/lang} -
    - - - {event name='mergeFields'} -
    - - {event name='sections'} - -
    - - {csrfToken} -
    -
    - -{include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/userEmailAddressExport.tpl b/wcfsetup/install/files/acp/templates/userEmailAddressExport.tpl deleted file mode 100644 index b032e246b20..00000000000 --- a/wcfsetup/install/files/acp/templates/userEmailAddressExport.tpl +++ /dev/null @@ -1,66 +0,0 @@ -{include file='header' pageTitle='wcf.acp.user.exportEmailAddress'} - -
    -
    -

    {lang}wcf.acp.user.exportEmailAddress{/lang}

    -
    - - {hascontent} - - {/hascontent} -
    - -{include file='shared_formError'} - -
    -
    -

    {lang}wcf.acp.user.exportEmailAddress.markedUsers{/lang}

    - -
    - {implode from=$users item=$user}{$user}{/implode} -
    - - {event name='markedUserFields'} -
    - -
    -

    {lang}wcf.acp.user.exportEmailAddress.format{/lang}

    - -
    -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    - - {event name='emailAddressFormatFields'} -
    - - {event name='sections'} - -
    - - {csrfToken} -
    -
    - -{include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/userList.tpl b/wcfsetup/install/files/acp/templates/userList.tpl index 9007154c0e6..441ba65ac91 100644 --- a/wcfsetup/install/files/acp/templates/userList.tpl +++ b/wcfsetup/install/files/acp/templates/userList.tpl @@ -1,247 +1,40 @@ -{if $searchID} - {assign var='pageTitle' value='wcf.acp.user.search'} -{else} - {assign var='pageTitle' value='wcf.acp.user.list'} -{/if} - -{include file='header'} +{include file='header' pageTitle='wcf.acp.user.list'} {event name='javascriptInclude'}
    -

    {lang}{$pageTitle}{/lang}{if $items} {#$items}{/if}

    +

    {lang}wcf.acp.user.list{/lang}

    - {hascontent} -
    -{assign var=encodedURL value=$url|rawurlencode} -{assign var=encodedAction value=$action|rawurlencode} - -{if $pages > 1} -
    - - -
    -{/if} - -{if $users|count} -
    - - - - - - - - {foreach from=$columnHeads key=column item=columnLanguageVariable} - - {/foreach} - - {event name='columnHeads'} - - - - - {foreach from=$users item=user} - - - - - - - - {foreach from=$columnHeads key=column item=columnLanguageVariable} - {if $column === 'lastActivityTime'} - - {else} - {if $column === 'registrationDate'} - - {/if} - - {/if} - {/foreach} - - {event name='columns'} - - {/foreach} - -
    {lang}wcf.global.objectID{/lang}{lang}wcf.user.username{/lang}{lang}{$columnLanguageVariable}{/lang}
    - - - - {$user->userID}{unsafe:$user->getAvatar()->getImageTag(24)} - - {if $user->editable} - {$user->username} - {else} - {$user->username} - {/if} - - - - {if $user->banned} - - {icon name='lock'} - - {/if} - {if $user->activationCode != 0} - - {icon name='power-off'} - - {if !$user->getBlacklistMatches()|empty} - - {icon name='warning'} - - {/if} - {/if} - - - {if MODULE_USER_RANK} - {if $user->getUserTitle()} {$user->getUserTitle()}{/if} - {if $user->getRank() && $user->getRank()->rankImage} {unsafe:$user->getRank()->getImage()}{/if} - {/if} - - {if $user->isOnline()} - {lang}wcf.user.online{/lang} - - {elseif $user->lastActivityTime} - {time time=$user->lastActivityTime} - {/if} - - {if $__wcf->session->getPermission('admin.user.canViewIpAddress') && $user->registrationIpAddress} - {$user->getRegistrationIpAddress()} - {/if} - {if $columnValues[$user->userID][$column]|isset}{unsafe:$columnValues[$user->userID][$column]}{/if}
    -
    - -
    - {if $pages > 1} -
    - - -
    - {/if} - - {hascontent} - - {/hascontent} -
    -{else} - {lang}wcf.global.noItems{/lang} -{/if} +
    + {unsafe:$gridView->render()} +
    {include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/userMerge.tpl b/wcfsetup/install/files/acp/templates/userMerge.tpl deleted file mode 100644 index acb6b18815f..00000000000 --- a/wcfsetup/install/files/acp/templates/userMerge.tpl +++ /dev/null @@ -1,65 +0,0 @@ -{include file='header' pageTitle='wcf.acp.user.merge'} - -
    -
    -

    {lang}wcf.acp.user.merge{/lang}

    -
    - - {hascontent} - - {/hascontent} -
    - -{include file='shared_formError'} - -{lang}wcf.acp.user.merge.warning{/lang} - -
    -
    -

    {lang}wcf.acp.user.merge.markedUsers{/lang}

    - -
    - {implode from=$users item='user'}{$user}{/implode} -
    - - {event name='markedUserFields'} -
    - -
    -

    {lang}wcf.acp.user.merge.destination{/lang}

    - - -
    -
    - - - {if $errorField == 'destinationUserID'} - - {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if} - - {/if} - {lang}wcf.acp.user.merge.destination.description{/lang} -
    - - - {event name='mergeFields'} -
    - - {event name='sections'} - -
    - - {csrfToken} -
    -
    - -{include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/userOptionSetDefaults.tpl b/wcfsetup/install/files/acp/templates/userOptionSetDefaults.tpl index a9a3569ddb2..93e71ce369d 100644 --- a/wcfsetup/install/files/acp/templates/userOptionSetDefaults.tpl +++ b/wcfsetup/install/files/acp/templates/userOptionSetDefaults.tpl @@ -4,7 +4,7 @@

    {lang}wcf.acp.user.option.setDefaults{/lang}

    - + {hascontent}