Rename grid view namespace for consistent naming
authorMarcel Werk <burntime@woltlab.com>
Tue, 12 Nov 2024 16:08:39 +0000 (17:08 +0100)
committerMarcel Werk <burntime@woltlab.com>
Tue, 12 Nov 2024 16:08:39 +0000 (17:08 +0100)
64 files changed:
wcfsetup/install/files/lib/acp/page/CronjobLogListPage.class.php
wcfsetup/install/files/lib/acp/page/ExceptionLogViewPage.class.php
wcfsetup/install/files/lib/acp/page/UserOptionListPage.class.php
wcfsetup/install/files/lib/acp/page/UserRankListPage.class.php
wcfsetup/install/files/lib/event/gridView/CronjobLogGridViewInitialized.class.php
wcfsetup/install/files/lib/event/gridView/ExceptionLogGridViewInitialized.class.php
wcfsetup/install/files/lib/event/gridView/UserOptionGridViewInitialized.class.php
wcfsetup/install/files/lib/event/gridView/UserRankGridViewInitialized.class.php
wcfsetup/install/files/lib/page/AbstractGridViewPage.class.php
wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php
wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/CronjobLogGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/DataSourceGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/DatabaseObjectListGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/ExceptionLogGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/GridViewRowLink.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/UserOptionGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/UserRankGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/filter/I18nTextFilter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/filter/IGridViewFilter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/filter/SelectFilter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/filter/TextFilter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/filter/TimeFilter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/AbstractColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/DefaultColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/IColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/LinkColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/NumberColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/PhraseColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/TimeColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/gridView/renderer/TitleColumnRenderer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/CronjobLogGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/DataSourceGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/ExceptionLogGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/GridViewRowLink.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/UserOptionGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/action/AbstractAction.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/action/DeleteAction.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/action/EditAction.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/action/IGridViewAction.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/action/ToggleAction.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/filter/I18nTextFilter.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/filter/SelectFilter.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/filter/TimeFilter.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/AbstractColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/DefaultColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/IColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/LinkColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/NumberColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/PhraseColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/TimeColumnRenderer.class.php [deleted file]
wcfsetup/install/files/lib/system/view/grid/renderer/TitleColumnRenderer.class.php [deleted file]

index 74548218b63d2874dc7c12d48c5c47fe6c9bdebe..ecdebc25be34abcb091ddbcea1d1c19125ab71d1 100755 (executable)
@@ -3,8 +3,8 @@
 namespace wcf\acp\page;
 
 use wcf\page\AbstractGridViewPage;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\CronjobLogGridView;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\CronjobLogGridView;
 
 /**
  * Shows cronjob log information.
index 8f3dc20cc588ac3632ab0cced38c62ccdfa6e7ea..5fd9ca0d0e606edbf37e0aa2008f82b44efc35ad 100644 (file)
@@ -3,9 +3,9 @@
 namespace wcf\acp\page;
 
 use wcf\page\AbstractGridViewPage;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\ExceptionLogGridView;
 use wcf\system\registry\RegistryHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\ExceptionLogGridView;
 
 /**
  * Shows the exception log.
index 059c60b9099cd83be7b4d873cd7f3f06e00a9e32..b1b1ad9882d4bcbce9e99a25900376e7a87a5a3f 100644 (file)
@@ -3,8 +3,8 @@
 namespace wcf\acp\page;
 
 use wcf\page\AbstractGridViewPage;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\UserOptionGridView;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\UserOptionGridView;
 
 /**
  * Shows a list of the installed user options.
index a487e7e0f269742eeecf138ffe189164247dbd1d..f2d80bf03652bb9ba956c273a868d271095a8990 100644 (file)
@@ -3,8 +3,8 @@
 namespace wcf\acp\page;
 
 use wcf\page\AbstractGridViewPage;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\UserRankGridView;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\UserRankGridView;
 
 /**
  * Lists available user ranks.
index 07012b241371a4851597686405cd65c5fe21914d..53380598e28d68373e4ae4b6bbd7e9727cdfee0b 100644 (file)
@@ -3,7 +3,7 @@
 namespace wcf\event\gridView;
 
 use wcf\event\IPsr14Event;
-use wcf\system\view\grid\CronjobLogGridView;
+use wcf\system\gridView\CronjobLogGridView;
 
 /**
  * Indicates that the cronjob log grid view has been initialized.
index 5e10db0a7b56918c088dfa4a19fb1db5f57a793e..3a8fa46f3b1cae60d6a60d810564cd929bea5399 100644 (file)
@@ -3,7 +3,7 @@
 namespace wcf\event\gridView;
 
 use wcf\event\IPsr14Event;
-use wcf\system\view\grid\ExceptionLogGridView;
+use wcf\system\gridView\ExceptionLogGridView;
 
 /**
  * Indicates that the exception log grid view has been initialized.
index 60af1a4dddd168718b047c68171e05adc03e6f12..a5c638cd1c9d213cfa4973c956d09acaa891c0c5 100644 (file)
@@ -3,7 +3,7 @@
 namespace wcf\event\gridView;
 
 use wcf\event\IPsr14Event;
-use wcf\system\view\grid\UserOptionGridView;
+use wcf\system\gridView\UserOptionGridView;
 
 /**
  * Indicates that the user option grid view has been initialized.
index 4eb06b4247182ddea6a76a7306c5c4bce7e59f8b..66f739a9c8f9f1d4aa9f8be71c935a79478fab33 100644 (file)
@@ -3,7 +3,7 @@
 namespace wcf\event\gridView;
 
 use wcf\event\IPsr14Event;
-use wcf\system\view\grid\UserRankGridView;
+use wcf\system\gridView\UserRankGridView;
 
 /**
  * Indicates that the user rank grid view has been initialized.
index fcc7029e474d60a50f82028d8cc99e1ec8db5c95..46eee81332ddee38470ff358cec2fa291942768b 100644 (file)
@@ -3,7 +3,7 @@
 namespace wcf\page;
 
 use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
+use wcf\system\gridView\AbstractGridView;
 use wcf\system\WCF;
 
 abstract class AbstractGridViewPage extends AbstractPage
index ffcb153ba99c94ba4ed0f4f873ffa88d4f80ca1a..dd5b977b831844867050259ca34bb2d4c3668596 100644 (file)
@@ -10,7 +10,7 @@ use wcf\system\endpoint\GetRequest;
 use wcf\system\endpoint\IController;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
-use wcf\system\view\grid\AbstractGridView;
+use wcf\system\gridView\AbstractGridView;
 
 #[GetRequest('/core/gridViews/rows')]
 final class GetRows implements IController
diff --git a/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php b/wcfsetup/install/files/lib/system/gridView/AbstractGridView.class.php
new file mode 100644 (file)
index 0000000..855dc37
--- /dev/null
@@ -0,0 +1,391 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use LogicException;
+use wcf\action\GridViewFilterAction;
+use wcf\event\IPsr14Event;
+use wcf\system\event\EventHandler;
+use wcf\system\gridView\action\IGridViewAction;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+abstract class AbstractGridView
+{
+    /**
+     * @var GridViewColumn[]
+     */
+    private array $columns = [];
+
+    /**
+     * @var IGridViewAction[]
+     */
+    private array $actions = [];
+
+    private GridViewRowLink $rowLink;
+    private int $rowsPerPage = 20;
+    private string $baseUrl = '';
+    private string $sortField = '';
+    private string $sortOrder = 'ASC';
+    private int $pageNo = 1;
+    private array $activeFilters = [];
+
+    /**
+     * Adds a new column to the grid view.
+     */
+    public function addColumn(GridViewColumn $column): void
+    {
+        $this->columns[] = $column;
+    }
+
+    /**
+     * Adds a new column to the grid view at the position before the specific id.
+     */
+    public function addColumnBefore(GridViewColumn $column, string $beforeID): void
+    {
+        $position = -1;
+
+        foreach ($this->getColumns() as $key => $existingColumn) {
+            if ($existingColumn->getID() === $beforeID) {
+                $position = $key;
+                break;
+            }
+        }
+
+        if ($position === -1) {
+            throw new \InvalidArgumentException("Invalid column id '{$beforeID}' given.");
+        }
+
+        array_splice($this->columns, $position, 0, [
+            $column,
+        ]);
+    }
+
+    /**
+     * Adds a new column to the grid view at the position after the specific id.
+     */
+    public function addColumnAfter(GridViewColumn $column, string $afterID): void
+    {
+        $position = -1;
+
+        foreach ($this->getColumns() as $key => $existingColumn) {
+            if ($existingColumn->getID() === $afterID) {
+                $position = $key;
+                break;
+            }
+        }
+
+        if ($position === -1) {
+            throw new \InvalidArgumentException("Invalid column id '{$afterID}' given.");
+        }
+
+        array_splice($this->columns, $position + 1, 0, [
+            $column,
+        ]);
+    }
+
+    /**
+     * Adds multiple new columns to the grid view.
+     * @param GridViewColumn[] $columns
+     */
+    public function addColumns(array $columns): void
+    {
+        foreach ($columns as $column) {
+            $this->addColumn($column);
+        }
+    }
+
+    /**
+     * Returns all columns of the grid view.
+     * @return GridViewColumn[]
+     */
+    public function getColumns(): array
+    {
+        return $this->columns;
+    }
+
+    /**
+     * Returns all visible (non-hidden) columns of the grid view.
+     * @return GridViewColumn[]
+     */
+    public function getVisibleColumns(): array
+    {
+        return \array_filter($this->getColumns(), fn($column) => !$column->isHidden());
+    }
+
+    /**
+     * Returns the column with the given id or null if no such column exists.
+     */
+    public function getColumn(string $id): ?GridViewColumn
+    {
+        foreach ($this->getColumns() as $column) {
+            if ($column->getID() === $id) {
+                return $column;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @param IGridViewAction[] $columns
+     */
+    public function addActions(array $actions): void
+    {
+        foreach ($actions as $action) {
+            $this->addAction($action);
+        }
+    }
+
+    public function addAction(IGridViewAction $action): void
+    {
+        $this->actions[] = $action;
+    }
+
+    /**
+     * @return IGridViewAction[]
+     */
+    public function getActions(): array
+    {
+        return $this->actions;
+    }
+
+    public function hasActions(): bool
+    {
+        return $this->actions !== [];
+    }
+
+    public function hasDropdownActions(): bool
+    {
+        return $this->getDropdownActions() !== [];
+    }
+
+    /**
+     * @return IGridViewAction[]
+     */
+    public function getDropdownActions(): array
+    {
+        return \array_filter($this->getActions(), fn($action) => !$action->isQuickAction());
+    }
+
+    /**
+     * @return IGridViewAction[]
+     */
+    public function getQuickActions(): array
+    {
+        return \array_filter($this->getActions(), fn($action) => $action->isQuickAction());
+    }
+
+    public function render(): string
+    {
+        return WCF::getTPL()->fetch('shared_gridView', 'wcf', ['view' => $this], true);
+    }
+
+    public function renderRows(): string
+    {
+        return WCF::getTPL()->fetch('shared_gridViewRows', 'wcf', ['view' => $this], true);
+    }
+
+    public function renderColumn(GridViewColumn $column, mixed $row): string
+    {
+        $value = $column->render($this->getData($row, $column->getID()), $row);
+
+        if (isset($this->rowLink)) {
+            $value = $this->rowLink->render($value, $row, $column->isTitleColumn());
+        }
+
+        return $value;
+    }
+
+    public function renderAction(IGridViewAction $action, mixed $row): string
+    {
+        return $action->render($row);
+    }
+
+    public function renderActionInitialization(): string
+    {
+        return implode(
+            "\n",
+            \array_map(
+                fn($action) => $action->renderInitialization($this),
+                $this->getActions()
+            )
+        );
+    }
+
+    protected function getData(mixed $row, string $identifer): mixed
+    {
+        return $row[$identifer] ?? '';
+    }
+
+    public abstract function getRows(): array;
+
+    public abstract function countRows(): int;
+
+    public function countPages(): int
+    {
+        return \ceil($this->countRows() / $this->getRowsPerPage());
+    }
+
+    public function getClassName(): string
+    {
+        return \get_class($this);
+    }
+
+    public function isAccessible(): bool
+    {
+        return true;
+    }
+
+    public function getID(): string
+    {
+        $classNamePieces = \explode('\\', static::class);
+
+        return \implode('-', $classNamePieces);
+    }
+
+    public function setBaseUrl(string $url): void
+    {
+        $this->baseUrl = $url;
+    }
+
+    public function getBaseUrl(): string
+    {
+        return $this->baseUrl;
+    }
+
+    /**
+     * @return GridViewColumn[]
+     */
+    public function getSortableColumns(): array
+    {
+        return \array_filter($this->getColumns(), fn($column) => $column->isSortable());
+    }
+
+    /**
+     * @return GridViewColumn[]
+     */
+    public function getFilterableColumns(): array
+    {
+        return \array_filter($this->getColumns(), fn($column) => $column->getFilter() !== null);
+    }
+
+    public function setSortField(string $sortField): void
+    {
+        if (!\in_array($sortField, \array_map(fn($column) => $column->getID(), $this->getSortableColumns()))) {
+            throw new \InvalidArgumentException("Invalid value '{$sortField}' as sort field given.");
+        }
+
+        $this->sortField = $sortField;
+    }
+
+    public function setSortOrder(string $sortOrder): void
+    {
+        if ($sortOrder !== 'ASC' && $sortOrder !== 'DESC') {
+            throw new \InvalidArgumentException("Invalid value '{$sortOrder}' as sort order given.");
+        }
+
+        $this->sortOrder = $sortOrder;
+    }
+
+    public function getSortField(): string
+    {
+        return $this->sortField;
+    }
+
+    public function getSortOrder(): string
+    {
+        return $this->sortOrder;
+    }
+
+    public function getPageNo(): int
+    {
+        return $this->pageNo;
+    }
+
+    public function setPageNo(int $pageNo): void
+    {
+        $this->pageNo = $pageNo;
+    }
+
+    public function getRowsPerPage(): int
+    {
+        return $this->rowsPerPage;
+    }
+
+    public function setRowsPerPage(int $rowsPerPage): void
+    {
+        $this->rowsPerPage = $rowsPerPage;
+    }
+
+    public function isFilterable(): bool
+    {
+        return $this->getFilterableColumns() !== [];
+    }
+
+    public function getFilterActionEndpoint(): string
+    {
+        return LinkHandler::getInstance()->getControllerLink(
+            GridViewFilterAction::class,
+            ['gridView' => \get_class($this)]
+        );
+    }
+
+    public function setActiveFilters(array $filters): void
+    {
+        $this->activeFilters = $filters;
+    }
+
+    public function getActiveFilters(): array
+    {
+        return $this->activeFilters;
+    }
+
+    public function getFilterLabel(string $id): string
+    {
+        $column = $this->getColumn($id);
+        if (!$column) {
+            throw new LogicException("Unknown column '" . $id . "'.");
+        }
+
+        if (!$column->getFilter()) {
+            throw new LogicException("Column '" . $id . "' has no filter.");
+        }
+
+        if (!isset($this->activeFilters[$id])) {
+            throw new LogicException("No value for filter '" . $id . "' found.");
+        }
+
+        return $column->getLabel() . ': ' . $column->getFilter()->renderValue($this->activeFilters[$id]);
+    }
+
+    public function getParameters(): array
+    {
+        return [];
+    }
+
+    public function addRowLink(GridViewRowLink $rowLink): void
+    {
+        $this->rowLink = $rowLink;
+    }
+
+    public function getObjectID(mixed $row): mixed
+    {
+        return '';
+    }
+
+    protected function fireInitializedEvent(): void
+    {
+        $event = $this->getInitializedEvent();
+        if ($event === null) {
+            return;
+        }
+
+        EventHandler::getInstance()->fire($event);
+    }
+
+    protected function getInitializedEvent(): ?IPsr14Event
+    {
+        return null;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/CronjobLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/CronjobLogGridView.class.php
new file mode 100644 (file)
index 0000000..3c9f634
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\data\cronjob\Cronjob;
+use wcf\data\cronjob\I18nCronjobList;
+use wcf\data\cronjob\log\CronjobLog;
+use wcf\data\cronjob\log\CronjobLogList;
+use wcf\data\DatabaseObjectList;
+use wcf\event\gridView\CronjobLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\filter\TimeFilter;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+final class CronjobLogGridView extends DatabaseObjectListGridView
+{
+    public function __construct()
+    {
+        $availableCronjobs = $this->getAvailableCronjobs();
+
+        $this->addColumns([
+            GridViewColumn::for('cronjobLogID')
+                ->label('wcf.global.objectID')
+                ->renderer(new NumberColumnRenderer())
+                ->sortable(),
+            GridViewColumn::for('cronjobID')
+                ->label('wcf.acp.cronjob')
+                ->sortable()
+                ->filter(new SelectFilter($availableCronjobs))
+                ->renderer([
+                    new class($availableCronjobs) extends TitleColumnRenderer {
+                        public function __construct(private readonly array $availableCronjobs) {}
+
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            return $this->availableCronjobs[$value];
+                        }
+                    },
+                ]),
+            GridViewColumn::for('execTime')
+                ->label('wcf.acp.cronjob.log.execTime')
+                ->sortable()
+                ->filter(new TimeFilter())
+                ->renderer(new TimeColumnRenderer()),
+            GridViewColumn::for('success')
+                ->label('wcf.acp.cronjob.log.status')
+                ->sortable()
+                ->filter(new SelectFilter([
+                    1 => 'wcf.acp.cronjob.log.success',
+                    0 => 'wcf.acp.cronjob.log.error',
+                ]))
+                ->renderer([
+                    new class extends DefaultColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            \assert($context instanceof CronjobLog);
+
+                            if ($context->success) {
+                                return '<span class="badge green">' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . '</span>';
+                            }
+                            if ($context->error) {
+                                $label = WCF::getLanguage()->get('wcf.acp.cronjob.log.error');
+                                $buttonId = 'cronjobLogErrorButton' . $context->cronjobLogID;
+                                $id = 'cronjobLogError' . $context->cronjobLogID;
+                                $error = StringUtil::encodeHTML($context->error);
+                                $dialogTitle = StringUtil::encodeJS(WCF::getLanguage()->get('wcf.acp.cronjob.log.error.details'));
+
+                                return <<<HTML
+                                    <button type="button" id="{$buttonId}" class="badge red">
+                                        {$label}
+                                    </button>
+                                    <template id="{$id}">{$error}</template>
+                                    <script data-relocate="true">
+                                        require(['WoltLabSuite/Core/Component/Dialog'], ({ dialogFactory }) => {
+                                            document.getElementById('{$buttonId}').addEventListener('click', () => {
+                                                const dialog = dialogFactory().fromId('{$id}').withoutControls();
+                                                dialog.show('{$dialogTitle}');
+                                            });
+                                        });
+                                    </script>
+                                    HTML;
+                            }
+
+                            return '';
+                        }
+                    },
+                ]),
+        ]);
+
+        $this->setSortField('execTime');
+        $this->setSortOrder('DESC');
+    }
+
+    #[\Override]
+    public function isAccessible(): bool
+    {
+        return WCF::getSession()->getPermission('admin.management.canManageCronjob');
+    }
+
+    #[\Override]
+    protected function createObjectList(): DatabaseObjectList
+    {
+        return new CronjobLogList();
+    }
+
+    #[\Override]
+    protected function getInitializedEvent(): ?IPsr14Event
+    {
+        return new CronjobLogGridViewInitialized($this);
+    }
+
+    private function getAvailableCronjobs(): array
+    {
+        $list = new I18nCronjobList();
+        $list->sqlOrderBy = 'descriptionI18n';
+        $list->readObjects();
+
+        return \array_map(fn(Cronjob $cronjob) => $cronjob->getDescription(), $list->getObjects());
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/DataSourceGridView.class.php b/wcfsetup/install/files/lib/system/gridView/DataSourceGridView.class.php
new file mode 100644 (file)
index 0000000..99b4aca
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use LogicException;
+
+abstract class DataSourceGridView extends AbstractGridView
+{
+    protected array $dataSource;
+
+    public function getRows(): array
+    {
+        $this->sortRows();
+
+        return $this->getRowsForPage();
+    }
+
+    protected function sortRows(): void
+    {
+        $this->getDataSource();
+
+        \uasort($this->dataSource, function (array $a, array $b) {
+            if ($this->getSortOrder() === 'ASC') {
+                return \strcmp($a[$this->getSortField()], $b[$this->getSortField()]);
+            } else {
+                return \strcmp($b[$this->getSortField()], $a[$this->getSortField()]);
+            }
+        });
+    }
+
+    protected function getRowsForPage(): array
+    {
+        return \array_slice($this->getDataSource(), ($this->getPageNo() - 1) * $this->getRowsPerPage(), $this->getRowsPerPage());
+    }
+
+    public function countRows(): int
+    {
+        return \count($this->getDataSource());
+    }
+
+    protected function getDataSource(): array
+    {
+        if (!isset($this->dataSource)) {
+            $this->dataSource = $this->loadDataSource();
+            $this->applyFilters();
+            $this->fireInitializedEvent();
+        }
+
+        return $this->dataSource;
+    }
+
+    protected function applyFilters(): void
+    {
+        foreach ($this->getActiveFilters() as $key => $value) {
+            $column = $this->getColumn($key);
+            if (!$column) {
+                throw new LogicException("Unknown column '" . $key . "'");
+            }
+
+            $this->dataSource = \array_filter($this->dataSource, function (array $row) use ($column, $value) {
+                return $column->getFilter()->matches($value, $row[$column->getID()]);
+            });
+        }
+    }
+
+    protected abstract function loadDataSource(): array;
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/DatabaseObjectListGridView.class.php b/wcfsetup/install/files/lib/system/gridView/DatabaseObjectListGridView.class.php
new file mode 100644 (file)
index 0000000..d8eacc2
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use LogicException;
+use wcf\data\DatabaseObject;
+use wcf\data\DatabaseObjectList;
+
+abstract class DatabaseObjectListGridView extends AbstractGridView
+{
+    protected DatabaseObjectList $objectList;
+    private int $objectCount;
+
+    public function getRows(): array
+    {
+        $this->getObjectList()->readObjects();
+
+        return $this->getObjectList()->getObjects();
+    }
+
+    public function countRows(): int
+    {
+        if (!isset($this->objectCount)) {
+            $this->objectCount = $this->getObjectList()->countObjects();
+        }
+
+        return $this->objectCount;
+    }
+
+    protected function getData(mixed $row, string $identifer): mixed
+    {
+        \assert($row instanceof DatabaseObject);
+
+        return $row->__get($identifer);
+    }
+
+    protected function initObjectList(): void
+    {
+        $this->objectList = $this->createObjectList();
+        $this->objectList->sqlLimit = $this->getRowsPerPage();
+        $this->objectList->sqlOffset = ($this->getPageNo() - 1) * $this->getRowsPerPage();
+        if ($this->getSortField()) {
+            $column = $this->getColumn($this->getSortField());
+            if ($column && $column->getSortById()) {
+                $this->objectList->sqlOrderBy = $column->getSortById() . ' ' . $this->getSortOrder();
+            } else {
+                $this->objectList->sqlOrderBy = $this->getSortField() . ' ' . $this->getSortOrder();
+            }
+        }
+        $this->applyFilters();
+        $this->fireInitializedEvent();
+    }
+
+    public function getObjectList(): DatabaseObjectList
+    {
+        if (!isset($this->objectList)) {
+            $this->initObjectList();
+        }
+
+        return $this->objectList;
+    }
+
+    protected function applyFilters(): void
+    {
+        foreach ($this->getActiveFilters() as $key => $value) {
+            $column = $this->getColumn($key);
+            if (!$column) {
+                throw new LogicException("Unknown column '" . $key . "'");
+            }
+
+            $column->getFilter()->applyFilter($this->getObjectList(), $column->getID(), $value);
+        }
+    }
+
+    #[\Override]
+    public function getObjectID(mixed $row): mixed
+    {
+        \assert($row instanceof DatabaseObject);
+
+        return $row->getObjectID();
+    }
+
+    protected abstract function createObjectList(): DatabaseObjectList;
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/ExceptionLogGridView.class.php b/wcfsetup/install/files/lib/system/gridView/ExceptionLogGridView.class.php
new file mode 100644 (file)
index 0000000..c1412af
--- /dev/null
@@ -0,0 +1,155 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\event\gridView\ExceptionLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\Regex;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\filter\TextFilter;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\DirectoryUtil;
+use wcf\util\ExceptionLogUtil;
+
+final class ExceptionLogGridView extends DataSourceGridView
+{
+    private array $availableLogFiles;
+
+    public function __construct(bool $applyDefaultFilter = false)
+    {
+        $this->addColumns([
+            GridViewColumn::for('message')
+                ->label('wcf.acp.exceptionLog.exception.message')
+                ->sortable()
+                ->renderer(new TitleColumnRenderer()),
+            GridViewColumn::for('exceptionID')
+                ->label('wcf.acp.exceptionLog.search.exceptionID')
+                ->filter(new TextFilter())
+                ->sortable(),
+            GridViewColumn::for('date')
+                ->label('wcf.acp.exceptionLog.exception.date')
+                ->sortable()
+                ->renderer(new TimeColumnRenderer()),
+            GridViewColumn::for('logFile')
+                ->label('wcf.acp.exceptionLog.search.logFile')
+                ->filter(new SelectFilter($this->getAvailableLogFiles()))
+                ->hidden(true),
+        ]);
+
+        $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
+        $this->setSortField('date');
+        $this->setSortOrder('DESC');
+
+        if ($applyDefaultFilter && $this->getDefaultLogFile() !== null) {
+            $this->setActiveFilters([
+                'logFile' => $this->getDefaultLogFile(),
+            ]);
+        }
+    }
+
+    #[\Override]
+    public function isAccessible(): bool
+    {
+        return WCF::getSession()->getPermission('admin.management.canViewLog');
+    }
+
+    #[\Override]
+    public function getObjectID(mixed $row): mixed
+    {
+        return $row['exceptionID'];
+    }
+
+    #[\Override]
+    protected function loadDataSource(): array
+    {
+        if (!empty($this->getActiveFilters()['exceptionID'])) {
+            $exceptionID = $this->getActiveFilters()['exceptionID'];
+            $contents = $logFile = '';
+            foreach ($this->getAvailableLogFiles() as $logFile) {
+                $contents = \file_get_contents(WCF_DIR . $logFile);
+
+                if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
+                    break;
+                }
+
+                unset($contents);
+            }
+
+            if ($contents === '') {
+                return [];
+            }
+
+            $exceptions = ExceptionLogUtil::splitLog($contents);
+            $parsedExceptions = [];
+
+            foreach ($exceptions as $key => $val) {
+                if ($key !== $exceptionID) {
+                    continue;
+                }
+
+                $parsed = ExceptionLogUtil::parseException($val);
+
+                $parsedExceptions[$key] = [
+                    'exceptionID' => $key,
+                    'message' => $parsed['message'],
+                    'date' => $parsed['date'],
+                    'logFile' => $logFile,
+                ];
+            }
+
+            return $parsedExceptions;
+        } elseif (!empty($this->getActiveFilters()['logFile'])) {
+            $contents = \file_get_contents(WCF_DIR . $this->getActiveFilters()['logFile']);
+            $exceptions = ExceptionLogUtil::splitLog($contents);
+            $parsedExceptions = [];
+
+            foreach ($exceptions as $key => $val) {
+                $parsed = ExceptionLogUtil::parseException($val);
+
+                $parsedExceptions[$key] = [
+                    'exceptionID' => $key,
+                    'message' => $parsed['message'],
+                    'date' => $parsed['date'],
+                    'logFile' => $this->getActiveFilters()['logFile'],
+                ];
+            }
+
+            return $parsedExceptions;
+        }
+
+        return [];
+    }
+
+    #[\Override]
+    protected function applyFilters(): void
+    {
+        // Overwrite the default filtering, as this is already applied when the data is loaded.
+    }
+
+    private function getAvailableLogFiles(): array
+    {
+        if (!isset($this->availableLogFiles)) {
+            $this->availableLogFiles = [];
+            $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
+            $logFiles = DirectoryUtil::getInstance(WCF_DIR . 'log/', false)->getFiles(\SORT_DESC, $fileNameRegex);
+            foreach ($logFiles as $logFile) {
+                $this->availableLogFiles['log/' . $logFile] = 'log/' . $logFile;
+            }
+        }
+
+        return $this->availableLogFiles;
+    }
+
+    private function getDefaultLogFile(): ?string
+    {
+        return \array_key_first($this->getAvailableLogFiles());
+    }
+
+    #[\Override]
+    protected function getInitializedEvent(): ?IPsr14Event
+    {
+        return new ExceptionLogGridViewInitialized($this);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php b/wcfsetup/install/files/lib/system/gridView/GridViewColumn.class.php
new file mode 100644 (file)
index 0000000..b8108d5
--- /dev/null
@@ -0,0 +1,174 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\gridView\filter\IGridViewFilter;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\IColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+
+final class GridViewColumn
+{
+    /**
+     * @var IColumnRenderer[]
+     */
+    private array $renderer = [];
+    private string $label = '';
+    private static DefaultColumnRenderer $defaultRenderer;
+    private bool $sortable = false;
+    private string $sortById = '';
+    private ?IGridViewFilter $filter = null;
+    private bool $hidden = false;
+
+    private function __construct(private readonly string $id) {}
+
+    public static function for(string $id): static
+    {
+        return new static($id);
+    }
+
+    public function render(mixed $value, mixed $context = null): string
+    {
+        if ($this->getRenderers() === []) {
+            return self::getDefaultRenderer()->render($value, $context);
+        }
+
+        foreach ($this->getRenderers() as $renderer) {
+            $value = $renderer->render($value, $context);
+        }
+
+        return $value;
+    }
+
+    public function getClasses(): string
+    {
+        if ($this->getRenderers() === []) {
+            return self::getDefaultRenderer()->getClasses();
+        }
+
+        return \implode(' ', \array_map(
+            static function (IColumnRenderer $renderer) {
+                return $renderer->getClasses();
+            },
+            $this->getRenderers()
+        ));
+    }
+
+    public function renderer(array|IColumnRenderer $renderers): static
+    {
+        if (!\is_array($renderers)) {
+            $renderers = [$renderers];
+        }
+
+        foreach ($renderers as $renderer) {
+            \assert($renderer instanceof IColumnRenderer);
+            $this->renderer[] = $renderer;
+        }
+
+        return $this;
+    }
+
+    public function label(string $languageItem): static
+    {
+        $this->label = WCF::getLanguage()->get($languageItem);
+
+        return $this;
+    }
+
+    public function sortable(bool $sortable = true): static
+    {
+        $this->sortable = $sortable;
+
+        return $this;
+    }
+
+    public function sortById(string $id): static
+    {
+        $this->sortById = $id;
+
+        return $this;
+    }
+
+    /**
+     * @return IColumnRenderer[]
+     */
+    public function getRenderers(): array
+    {
+        return $this->renderer;
+    }
+
+    public function getID(): string
+    {
+        return $this->id;
+    }
+
+    public function getLabel(): string
+    {
+        return $this->label;
+    }
+
+    public function isSortable(): bool
+    {
+        return $this->sortable;
+    }
+
+    public function getSortById(): string
+    {
+        return $this->sortById;
+    }
+
+    public function filter(?IGridViewFilter $filter): static
+    {
+        $this->filter = $filter;
+
+        return $this;
+    }
+
+    public function getFilter(): ?IGridViewFilter
+    {
+        return $this->filter;
+    }
+
+    public function getFilterFormField(): AbstractFormField
+    {
+        if ($this->getFilter() === null) {
+            throw new \LogicException('This column has no filter.');
+        }
+
+        return $this->getFilter()->getFormField($this->getID(), $this->getLabel());
+    }
+
+    private static function getDefaultRenderer(): DefaultColumnRenderer
+    {
+        if (!isset(self::$defaultRenderer)) {
+            self::$defaultRenderer = new DefaultColumnRenderer();
+        }
+
+        return self::$defaultRenderer;
+    }
+
+    public function isTitleColumn(): bool
+    {
+        foreach ($this->getRenderers() as $renderer) {
+            if ($renderer instanceof TitleColumnRenderer) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public function hidden(bool $hidden = true): static
+    {
+        $this->hidden = $hidden;
+
+        return $this;
+    }
+
+    public function isHidden(): bool
+    {
+        return $this->hidden;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/GridViewRowLink.class.php b/wcfsetup/install/files/lib/system/gridView/GridViewRowLink.class.php
new file mode 100644 (file)
index 0000000..5031df3
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\util\StringUtil;
+
+class GridViewRowLink
+{
+    public function __construct(
+        private readonly string $controllerClass = '',
+        private readonly array $parameters = [],
+        private readonly string $cssClass = ''
+    ) {}
+
+    public function render(mixed $value, mixed $context = null, bool $isPrimaryColumn = false): string
+    {
+        $href = '';
+        if ($this->controllerClass) {
+            \assert($context instanceof DatabaseObject);
+            $href = LinkHandler::getInstance()->getControllerLink(
+                $this->controllerClass,
+                \array_merge($this->parameters, ['object' => $context])
+            );
+        }
+
+        $attributes = [];
+        $isButton = true;
+        if ($href) {
+            $attributes[] = 'href="' . $href . '"';
+            $isButton = false;
+        }
+        $attributes[] = 'class="gridView__rowLink ' . StringUtil::encodeHTML($this->cssClass) . '"';
+        $attributes[] = 'tabindex="' . ($isPrimaryColumn ? '0' : '-1') . '"';
+
+        if ($isButton) {
+            return '<button type="button" ' . implode(' ', $attributes) . '>'
+                . $value
+                . '</button>';
+        } else {
+            return '<a ' . implode(' ', $attributes) . '>'
+                . $value
+                . '</a>';
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/UserOptionGridView.class.php b/wcfsetup/install/files/lib/system/gridView/UserOptionGridView.class.php
new file mode 100644 (file)
index 0000000..5e5d06c
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\acp\form\UserOptionEditForm;
+use wcf\data\DatabaseObjectList;
+use wcf\data\user\option\UserOption;
+use wcf\data\user\option\UserOptionList;
+use wcf\event\gridView\UserOptionGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\action\DeleteAction;
+use wcf\system\gridView\action\EditAction;
+use wcf\system\gridView\action\ToggleAction;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+final class UserOptionGridView extends DatabaseObjectListGridView
+{
+    public function __construct()
+    {
+        $this->addColumns([
+            GridViewColumn::for('optionID')
+                ->label('wcf.global.objectID')
+                ->renderer(new NumberColumnRenderer())
+                ->sortable(),
+            GridViewColumn::for('optionName')
+                ->label('wcf.global.name')
+                ->sortable()
+                ->renderer([
+                    new class extends TitleColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            \assert($context instanceof UserOption);
+
+                            return StringUtil::encodeHTML($context->getTitle());
+                        }
+                    }
+                ]),
+            GridViewColumn::for('categoryName')
+                ->label('wcf.global.category')
+                ->sortable()
+                ->renderer([
+                    new class extends DefaultColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            \assert($context instanceof UserOption);
+
+                            return StringUtil::encodeHTML(
+                                WCF::getLanguage()->get('wcf.user.option.category.' . $context->categoryName)
+                            );
+                        }
+                    }
+                ]),
+            GridViewColumn::for('optionType')
+                ->label('wcf.acp.user.option.optionType')
+                ->sortable(),
+            GridViewColumn::for('showOrder')
+                ->label('wcf.global.showOrder')
+                ->sortable()
+                ->renderer(new NumberColumnRenderer()),
+        ]);
+
+        $this->addActions([
+            new ToggleAction('core/users/options/%s/enable', 'core/users/options/%s/disable'),
+            new EditAction(UserOptionEditForm::class),
+            new DeleteAction('core/users/options/%s', fn(UserOption $row) => $row->canDelete()),
+        ]);
+        $this->addRowLink(new GridViewRowLink(UserOptionEditForm::class));
+        $this->setSortField('showOrder');
+    }
+
+    #[\Override]
+    public function isAccessible(): bool
+    {
+        return WCF::getSession()->getPermission('admin.user.canManageUserOption');
+    }
+
+    #[\Override]
+    protected function createObjectList(): DatabaseObjectList
+    {
+        $list = new UserOptionList();
+        $list->getConditionBuilder()->add(
+            "option_table.categoryName IN (
+                SELECT  categoryName
+                FROM    wcf" . WCF_N . "_user_option_category
+                WHERE   parentCategoryName = ?
+            )",
+            ['profile']
+        );
+
+        return $list;
+    }
+
+    #[\Override]
+    protected function getInitializedEvent(): ?IPsr14Event
+    {
+        return new UserOptionGridViewInitialized($this);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/gridView/UserRankGridView.class.php
new file mode 100644 (file)
index 0000000..30d2093
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\acp\form\UserRankEditForm;
+use wcf\data\DatabaseObjectList;
+use wcf\data\user\group\UserGroup;
+use wcf\data\user\rank\I18nUserRankList;
+use wcf\data\user\rank\UserRank;
+use wcf\event\gridView\UserRankGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\action\DeleteAction;
+use wcf\system\gridView\action\EditAction;
+use wcf\system\gridView\filter\I18nTextFilter;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+final class UserRankGridView extends DatabaseObjectListGridView
+{
+    public function __construct()
+    {
+        $this->addColumns([
+            GridViewColumn::for('rankID')
+                ->label('wcf.global.objectID')
+                ->renderer(new NumberColumnRenderer())
+                ->sortable(),
+            GridViewColumn::for('rankTitle')
+                ->label('wcf.acp.user.rank.title')
+                ->sortable()
+                ->sortById('rankTitleI18n')
+                ->filter(new I18nTextFilter())
+                ->renderer([
+                    new class extends TitleColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            \assert($context instanceof UserRank);
+
+                            return '<span class="badge label' . ($context->cssClassName ? ' ' . $context->cssClassName : '') . '">'
+                                . StringUtil::encodeHTML($context->getTitle())
+                                . '<span>';
+                        }
+                    }
+                ]),
+            GridViewColumn::for('rankImage')
+                ->label('wcf.acp.user.rank.image')
+                ->sortable()
+                ->renderer([
+                    new class extends DefaultColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            \assert($context instanceof UserRank);
+
+                            return $context->rankImage ? $context->getImage() : '';
+                        }
+                    },
+                ]),
+            GridViewColumn::for('groupID')
+                ->label('wcf.user.group')
+                ->sortable()
+                ->filter(new SelectFilter($this->getAvailableUserGroups()))
+                ->renderer([
+                    new class extends DefaultColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            return StringUtil::encodeHTML(UserGroup::getGroupByID($value)->getName());
+                        }
+                    },
+                ]),
+            GridViewColumn::for('requiredGender')
+                ->label('wcf.user.option.gender')
+                ->sortable()
+                ->renderer([
+                    new class extends DefaultColumnRenderer {
+                        public function render(mixed $value, mixed $context = null): string
+                        {
+                            if (!$value) {
+                                return '';
+                            }
+
+                            return WCF::getLanguage()->get(match ($value) {
+                                1 => 'wcf.user.gender.male',
+                                2 => 'wcf.user.gender.female',
+                                default => 'wcf.user.gender.other'
+                            });
+                        }
+                    },
+                ]),
+            GridViewColumn::for('requiredPoints')
+                ->label('wcf.acp.user.rank.requiredPoints')
+                ->sortable()
+                ->renderer(new NumberColumnRenderer()),
+        ]);
+
+        $this->addActions([
+            new EditAction(UserRankEditForm::class),
+            new DeleteAction('core/users/ranks/%s'),
+        ]);
+        $this->addRowLink(new GridViewRowLink(UserRankEditForm::class));
+        $this->setSortField('rankTitle');
+    }
+
+    #[\Override]
+    public function isAccessible(): bool
+    {
+        return \MODULE_USER_RANK && WCF::getSession()->getPermission('admin.user.rank.canManageRank');
+    }
+
+    #[\Override]
+    protected function createObjectList(): DatabaseObjectList
+    {
+        return new I18nUserRankList();
+    }
+
+    #[\Override]
+    protected function getInitializedEvent(): ?IPsr14Event
+    {
+        return new UserRankGridViewInitialized($this);
+    }
+
+    private function getAvailableUserGroups(): array
+    {
+        $groups = [];
+        foreach (UserGroup::getSortedGroupsByType([], [UserGroup::GUESTS, UserGroup::EVERYONE]) as $group) {
+            $groups[$group->groupID] = $group->getName();
+        }
+
+        return $groups;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/AbstractAction.class.php
new file mode 100644 (file)
index 0000000..169f667
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+
+abstract class AbstractAction implements IGridViewAction
+{
+    public function __construct(
+        private readonly ?Closure $isAvailableCallback = null
+    ) {}
+
+    #[\Override]
+    public function isAvailable(mixed $row): bool
+    {
+        if ($this->isAvailableCallback === null) {
+            return true;
+        }
+
+        return ($this->isAvailableCallback)($row);
+    }
+
+    #[\Override]
+    public function isQuickAction(): bool
+    {
+        return false;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/DeleteAction.class.php
new file mode 100644 (file)
index 0000000..0f52c3d
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\data\ITitledObject;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class DeleteAction extends AbstractAction
+{
+    public function __construct(
+        private readonly string $endpoint,
+        ?Closure $isAvailableCallback = null
+    ) {
+        parent::__construct($isAvailableCallback);
+    }
+
+    #[\Override]
+    public function render(mixed $row): string
+    {
+        \assert($row instanceof DatabaseObject);
+
+        $label = WCF::getLanguage()->get('wcf.global.button.delete');
+
+        if (!$this->isAvailable($row)) {
+            return <<<HTML
+                <span>
+                    {$label}
+                </span>
+                HTML;
+        }
+
+        $endpoint = StringUtil::encodeHTML(
+            LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+                \sprintf($this->endpoint, $row->getObjectID())
+        );
+        if ($row instanceof ITitledObject) {
+            $objectName = StringUtil::encodeHTML($row->getTitle());
+        } else {
+            $objectName = '';
+        }
+
+        return <<<HTML
+            <button type="button" data-action="delete" data-object-name="{$objectName}" data-endpoint="{$endpoint}">
+                {$label}
+            </button>
+            HTML;
+    }
+
+    #[\Override]
+    public function renderInitialization(AbstractGridView $gridView): ?string
+    {
+        $id = StringUtil::encodeJS($gridView->getID());
+
+        return <<<HTML
+            <script data-relocate="true">
+                require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => {
+                    setup(document.getElementById('{$id}_table'));
+                });
+            </script>
+            HTML;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/EditAction.class.php
new file mode 100644 (file)
index 0000000..aa9029b
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+use wcf\data\DatabaseObject;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+class EditAction extends AbstractAction
+{
+    public function __construct(
+        private readonly string $controllerClass,
+        ?Closure $isAvailableCallback = null
+    ) {
+        parent::__construct($isAvailableCallback);
+    }
+
+    #[\Override]
+    public function render(mixed $row): string
+    {
+        \assert($row instanceof DatabaseObject);
+        $href = LinkHandler::getInstance()->getControllerLink(
+            $this->controllerClass,
+            ['object' => $row]
+        );
+
+        return '<a href="' . $href . '">' . WCF::getLanguage()->get('wcf.global.button.edit') . '</a>';
+    }
+
+    #[\Override]
+    public function renderInitialization(AbstractGridView $gridView): ?string
+    {
+        return null;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/IGridViewAction.class.php
new file mode 100644 (file)
index 0000000..fdcc0b1
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace wcf\system\gridView\action;
+
+use wcf\system\gridView\AbstractGridView;
+
+interface IGridViewAction
+{
+    public function render(mixed $row): string;
+
+    public function renderInitialization(AbstractGridView $gridView): ?string;
+
+    public function isQuickAction(): bool;
+
+    public function isAvailable(mixed $row): bool;
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php b/wcfsetup/install/files/lib/system/gridView/action/ToggleAction.class.php
new file mode 100644 (file)
index 0000000..27d9fe9
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class ToggleAction extends AbstractAction
+{
+    public function __construct(
+        private readonly string $enableEndpoint,
+        private readonly string $disableEndpoint,
+        private readonly string $propertyName = 'isDisabled',
+        ?Closure $isAvailableCallback = null
+    ) {
+        parent::__construct($isAvailableCallback);
+    }
+
+    #[\Override]
+    public function render(mixed $row): string
+    {
+        \assert($row instanceof DatabaseObject);
+
+        $enableEndpoint = StringUtil::encodeHTML(
+            LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+                \sprintf($this->enableEndpoint, $row->getObjectID())
+        );
+        $disableEndpoint = StringUtil::encodeHTML(
+            LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+                \sprintf($this->disableEndpoint, $row->getObjectID())
+        );
+
+        $ariaLabel = WCF::getLanguage()->get('wcf.global.button.enable');
+        $checked = !$row->{$this->propertyName} ? 'checked' : '';
+
+        return <<<HTML
+            <woltlab-core-toggle-button aria-label="{$ariaLabel}" data-enable-endpoint="{$enableEndpoint}" data-disable-endpoint="{$disableEndpoint}" {$checked}></woltlab-core-toggle-button>
+            HTML;
+    }
+
+    #[\Override]
+    public function renderInitialization(AbstractGridView $gridView): ?string
+    {
+        $id = StringUtil::encodeJS($gridView->getID());
+
+        return <<<HTML
+            <script data-relocate="true">
+                require(['WoltLabSuite/Core/Component/GridView/Action/Toggle'], ({ setup }) => {
+                    setup('{$id}_table');
+                });
+            </script>
+            HTML;
+    }
+
+    #[\Override]
+    public function isQuickAction(): bool
+    {
+        return true;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/filter/I18nTextFilter.class.php b/wcfsetup/install/files/lib/system/gridView/filter/I18nTextFilter.class.php
new file mode 100644 (file)
index 0000000..e70eb2c
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\WCF;
+
+class I18nTextFilter extends TextFilter
+{
+    #[\Override]
+    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+    {
+        $list->getConditionBuilder()->add("($id LIKE ? OR $id IN (SELECT languageItem FROM wcf1_language_item WHERE languageID = ? AND languageItemValue LIKE ?))", [
+            '%' . $value . '%',
+            WCF::getLanguage()->languageID,
+            '%' . $value . '%'
+        ]);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/filter/IGridViewFilter.class.php b/wcfsetup/install/files/lib/system/gridView/filter/IGridViewFilter.class.php
new file mode 100644 (file)
index 0000000..43ef851
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+
+interface IGridViewFilter
+{
+    public function getFormField(string $id, string $label): AbstractFormField;
+
+    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void;
+
+    public function matches(string $filterValue, string $rowValue): bool;
+
+    public function renderValue(string $value): string;
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/filter/SelectFilter.class.php b/wcfsetup/install/files/lib/system/gridView/filter/SelectFilter.class.php
new file mode 100644 (file)
index 0000000..e595547
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\SelectFormField;
+use wcf\system\WCF;
+
+class SelectFilter implements IGridViewFilter
+{
+    public function __construct(private readonly array $options) {}
+
+    #[\Override]
+    public function getFormField(string $id, string $label): AbstractFormField
+    {
+        return SelectFormField::create($id)
+            ->label($label)
+            ->options($this->options);
+    }
+
+    #[\Override]
+    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+    {
+        $list->getConditionBuilder()->add("$id = ?", [$value]);
+    }
+
+    #[\Override]
+    public function matches(string $filterValue, string $rowValue): bool
+    {
+        return $filterValue === $rowValue;
+    }
+
+    #[\Override]
+    public function renderValue(string $value): string
+    {
+        return WCF::getLanguage()->get($this->options[$value]);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/filter/TextFilter.class.php b/wcfsetup/install/files/lib/system/gridView/filter/TextFilter.class.php
new file mode 100644 (file)
index 0000000..5201439
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\TextFormField;
+
+class TextFilter implements IGridViewFilter
+{
+    #[\Override]
+    public function getFormField(string $id, string $label): AbstractFormField
+    {
+        return TextFormField::create($id)
+            ->label($label);
+    }
+
+    #[\Override]
+    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+    {
+        $list->getConditionBuilder()->add("$id LIKE ?", ['%' . $value . '%']);
+    }
+
+    #[\Override]
+    public function matches(string $filterValue, string $rowValue): bool
+    {
+        return \str_contains(\mb_strtolower($rowValue), \mb_strtolower($filterValue));
+    }
+
+    #[\Override]
+    public function renderValue(string $value): string
+    {
+        return $value;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/filter/TimeFilter.class.php b/wcfsetup/install/files/lib/system/gridView/filter/TimeFilter.class.php
new file mode 100644 (file)
index 0000000..b00cca1
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\DateRangeFormField;
+use wcf\system\WCF;
+
+class TimeFilter implements IGridViewFilter
+{
+    #[\Override]
+    public function getFormField(string $id, string $label): AbstractFormField
+    {
+        return DateRangeFormField::create($id)
+            ->label($label)
+            ->supportTime();
+    }
+
+    #[\Override]
+    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+    {
+        $timestamps = $this->getTimestamps($value);
+
+        if (!$timestamps['from'] && !$timestamps['to']) {
+            return;
+        }
+
+        if (!$timestamps['to']) {
+            $list->getConditionBuilder()->add("$id >= ?", [$timestamps['from']]);
+        } else {
+            $list->getConditionBuilder()->add("$id BETWEEN ? AND ?", [$timestamps['from'], $timestamps['to']]);
+        }
+    }
+
+    #[\Override]
+    public function matches(string $filterValue, string $rowValue): bool
+    {
+        $timestamps = $this->getTimestamps($filterValue);
+
+        if (!$timestamps['from'] && !$timestamps['to']) {
+            return true;
+        }
+
+        if (!$timestamps['to']) {
+            return $rowValue >= $timestamps['from'];
+        } else {
+            return $rowValue >= $timestamps['from'] && $rowValue <= $timestamps['to'];
+        }
+    }
+
+    #[\Override]
+    public function renderValue(string $value): string
+    {
+        $values = explode(';', $value);
+        if (\count($values) !== 2) {
+            return '';
+        }
+
+        $locale = WCF::getLanguage()->getLocale();;
+        $fromString = $toString = '';
+        if ($values[0] !== '') {
+            $fromDateTime = \DateTime::createFromFormat(
+                'Y-m-d\TH:i:sP',
+                $values[0],
+                WCF::getUser()->getTimeZone()
+            );
+            if ($fromDateTime !== false) {
+                $fromString = \IntlDateFormatter::formatObject(
+                    $fromDateTime,
+                    [
+                        \IntlDateFormatter::LONG,
+                        \IntlDateFormatter::SHORT,
+                    ],
+                    $locale
+                );
+            }
+        }
+        if ($values[1] !== '') {
+            $toDateTime = \DateTime::createFromFormat(
+                'Y-m-d\TH:i:sP',
+                $values[1],
+                WCF::getUser()->getTimeZone()
+            );
+            if ($toDateTime !== false) {
+                $toString = \IntlDateFormatter::formatObject(
+                    $toDateTime,
+                    [
+                        \IntlDateFormatter::LONG,
+                        \IntlDateFormatter::SHORT,
+                    ],
+                    $locale
+                );
+            }
+        }
+
+        if ($fromString && $toString) {
+            return $fromString . ' ‐ ' . $toString;
+        } else if ($fromString) {
+            return '>= ' . $fromString;
+        } else if ($toString) {
+            return '<= ' . $toString;
+        }
+
+        return '';
+    }
+
+    private function getTimestamps(string $value): array
+    {
+        $from = 0;
+        $to = 0;
+
+        $values = explode(';', $value);
+        if (\count($values) === 2) {
+            $from = $this->getTimestamp($values[0]);
+            $to = $this->getTimestamp($values[1]);
+        }
+
+        return [
+            'from' => $from,
+            'to' => $to,
+        ];
+    }
+
+    private function getTimestamp(string $date): int
+    {
+        $dateTime = \DateTime::createFromFormat(
+            'Y-m-d\TH:i:sP',
+            $date
+        );
+
+        if ($dateTime !== false) {
+            return $dateTime->getTimestamp();
+        }
+
+        return 0;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/AbstractColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/AbstractColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..6540c87
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+abstract class AbstractColumnRenderer implements IColumnRenderer
+{
+    public function getClasses(): string
+    {
+        return '';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/DefaultColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/DefaultColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..90f4ea1
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\util\StringUtil;
+
+class DefaultColumnRenderer extends AbstractColumnRenderer
+{
+    public function render(mixed $value, mixed $context = null): string
+    {
+        return StringUtil::encodeHTML($value);
+    }
+
+    public function getClasses(): string
+    {
+        return 'gridView__column--text';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/IColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/IColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..acf0eb3
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+interface IColumnRenderer
+{
+    public function render(mixed $value, mixed $context = null): string;
+
+    public function getClasses(): string;
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/LinkColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/LinkColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..05aa62d
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+class LinkColumnRenderer extends AbstractColumnRenderer
+{
+    public function __construct(
+        private readonly string $controllerClass,
+        private readonly array $parameters = [],
+        private readonly string $titleLanguageItem = ''
+    ) {}
+
+    public function render(mixed $value, mixed $context = null): string
+    {
+        \assert($context instanceof DatabaseObject);
+        $href = LinkHandler::getInstance()->getControllerLink(
+            $this->controllerClass,
+            \array_merge($this->parameters, ['object' => $context])
+        );
+
+        return '<a href="' . $href . '"'
+            . ($this->titleLanguageItem ? ' title="' . WCF::getLanguage()->get($this->titleLanguageItem) . '"' : '') . '>'
+            . $value
+            . '</a>';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/NumberColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/NumberColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..845c91f
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\util\StringUtil;
+
+class NumberColumnRenderer extends AbstractColumnRenderer
+{
+    public function render(mixed $value, mixed $context = null): string
+    {
+        return StringUtil::formatNumeric($value);
+    }
+
+    public function getClasses(): string
+    {
+        return 'gridView__column--digits';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/PhraseColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/PhraseColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..3d4ccab
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class PhraseColumnRenderer extends DefaultColumnRenderer
+{
+    public function render(mixed $value, mixed $context = null): string
+    {
+        return StringUtil::encodeHTML(WCF::getLanguage()->get($value));
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/TimeColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/TimeColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..7784d9e
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\system\WCF;
+
+class TimeColumnRenderer extends AbstractColumnRenderer
+{
+    public function render(mixed $value, mixed $context = null): string
+    {
+        $timestamp = \intval($value);
+        if (!$timestamp) {
+            return '';
+        }
+
+        $dateTime = new \DateTimeImmutable('@' . $timestamp);
+        $dateTime = $dateTime->setTimezone(WCF::getUser()->getTimeZone());
+        $locale = WCF::getLanguage()->getLocale();
+
+        $isFutureDate = $dateTime->getTimestamp() > TIME_NOW;
+
+        $dateAndTime = \IntlDateFormatter::formatObject(
+            $dateTime,
+            [
+                \IntlDateFormatter::LONG,
+                \IntlDateFormatter::SHORT,
+            ],
+            $locale
+        );
+
+        return \sprintf(
+            '<woltlab-core-date-time date="%s"%s>%s</woltlab-core-date-time>',
+            $dateTime->format('c'),
+            $isFutureDate ? ' static' : '',
+            $dateAndTime
+        );
+    }
+
+    public function getClasses(): string
+    {
+        return 'gridView__column--date';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/renderer/TitleColumnRenderer.class.php b/wcfsetup/install/files/lib/system/gridView/renderer/TitleColumnRenderer.class.php
new file mode 100644 (file)
index 0000000..60d9154
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+class TitleColumnRenderer extends DefaultColumnRenderer
+{
+    public function getClasses(): string
+    {
+        return 'gridView__column--title';
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/AbstractGridView.class.php
deleted file mode 100644 (file)
index e66e04e..0000000
+++ /dev/null
@@ -1,391 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use LogicException;
-use wcf\action\GridViewFilterAction;
-use wcf\event\IPsr14Event;
-use wcf\system\event\EventHandler;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\action\IGridViewAction;
-use wcf\system\WCF;
-
-abstract class AbstractGridView
-{
-    /**
-     * @var GridViewColumn[]
-     */
-    private array $columns = [];
-
-    /**
-     * @var IGridViewAction[]
-     */
-    private array $actions = [];
-
-    private GridViewRowLink $rowLink;
-    private int $rowsPerPage = 20;
-    private string $baseUrl = '';
-    private string $sortField = '';
-    private string $sortOrder = 'ASC';
-    private int $pageNo = 1;
-    private array $activeFilters = [];
-
-    /**
-     * Adds a new column to the grid view.
-     */
-    public function addColumn(GridViewColumn $column): void
-    {
-        $this->columns[] = $column;
-    }
-
-    /**
-     * Adds a new column to the grid view at the position before the specific id.
-     */
-    public function addColumnBefore(GridViewColumn $column, string $beforeID): void
-    {
-        $position = -1;
-
-        foreach ($this->getColumns() as $key => $existingColumn) {
-            if ($existingColumn->getID() === $beforeID) {
-                $position = $key;
-                break;
-            }
-        }
-
-        if ($position === -1) {
-            throw new \InvalidArgumentException("Invalid column id '{$beforeID}' given.");
-        }
-
-        array_splice($this->columns, $position, 0, [
-            $column,
-        ]);
-    }
-
-    /**
-     * Adds a new column to the grid view at the position after the specific id.
-     */
-    public function addColumnAfter(GridViewColumn $column, string $afterID): void
-    {
-        $position = -1;
-
-        foreach ($this->getColumns() as $key => $existingColumn) {
-            if ($existingColumn->getID() === $afterID) {
-                $position = $key;
-                break;
-            }
-        }
-
-        if ($position === -1) {
-            throw new \InvalidArgumentException("Invalid column id '{$afterID}' given.");
-        }
-
-        array_splice($this->columns, $position + 1, 0, [
-            $column,
-        ]);
-    }
-
-    /**
-     * Adds multiple new columns to the grid view.
-     * @param GridViewColumn[] $columns
-     */
-    public function addColumns(array $columns): void
-    {
-        foreach ($columns as $column) {
-            $this->addColumn($column);
-        }
-    }
-
-    /**
-     * Returns all columns of the grid view.
-     * @return GridViewColumn[]
-     */
-    public function getColumns(): array
-    {
-        return $this->columns;
-    }
-
-    /**
-     * Returns all visible (non-hidden) columns of the grid view.
-     * @return GridViewColumn[]
-     */
-    public function getVisibleColumns(): array
-    {
-        return \array_filter($this->getColumns(), fn($column) => !$column->isHidden());
-    }
-
-    /**
-     * Returns the column with the given id or null if no such column exists.
-     */
-    public function getColumn(string $id): ?GridViewColumn
-    {
-        foreach ($this->getColumns() as $column) {
-            if ($column->getID() === $id) {
-                return $column;
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * @param IGridViewAction[] $columns
-     */
-    public function addActions(array $actions): void
-    {
-        foreach ($actions as $action) {
-            $this->addAction($action);
-        }
-    }
-
-    public function addAction(IGridViewAction $action): void
-    {
-        $this->actions[] = $action;
-    }
-
-    /**
-     * @return IGridViewAction[]
-     */
-    public function getActions(): array
-    {
-        return $this->actions;
-    }
-
-    public function hasActions(): bool
-    {
-        return $this->actions !== [];
-    }
-
-    public function hasDropdownActions(): bool
-    {
-        return $this->getDropdownActions() !== [];
-    }
-
-    /**
-     * @return IGridViewAction[]
-     */
-    public function getDropdownActions(): array
-    {
-        return \array_filter($this->getActions(), fn($action) => !$action->isQuickAction());
-    }
-
-    /**
-     * @return IGridViewAction[]
-     */
-    public function getQuickActions(): array
-    {
-        return \array_filter($this->getActions(), fn($action) => $action->isQuickAction());
-    }
-
-    public function render(): string
-    {
-        return WCF::getTPL()->fetch('shared_gridView', 'wcf', ['view' => $this], true);
-    }
-
-    public function renderRows(): string
-    {
-        return WCF::getTPL()->fetch('shared_gridViewRows', 'wcf', ['view' => $this], true);
-    }
-
-    public function renderColumn(GridViewColumn $column, mixed $row): string
-    {
-        $value = $column->render($this->getData($row, $column->getID()), $row);
-
-        if (isset($this->rowLink)) {
-            $value = $this->rowLink->render($value, $row, $column->isTitleColumn());
-        }
-
-        return $value;
-    }
-
-    public function renderAction(IGridViewAction $action, mixed $row): string
-    {
-        return $action->render($row);
-    }
-
-    public function renderActionInitialization(): string
-    {
-        return implode(
-            "\n",
-            \array_map(
-                fn($action) => $action->renderInitialization($this),
-                $this->getActions()
-            )
-        );
-    }
-
-    protected function getData(mixed $row, string $identifer): mixed
-    {
-        return $row[$identifer] ?? '';
-    }
-
-    public abstract function getRows(): array;
-
-    public abstract function countRows(): int;
-
-    public function countPages(): int
-    {
-        return \ceil($this->countRows() / $this->getRowsPerPage());
-    }
-
-    public function getClassName(): string
-    {
-        return \get_class($this);
-    }
-
-    public function isAccessible(): bool
-    {
-        return true;
-    }
-
-    public function getID(): string
-    {
-        $classNamePieces = \explode('\\', static::class);
-
-        return \implode('-', $classNamePieces);
-    }
-
-    public function setBaseUrl(string $url): void
-    {
-        $this->baseUrl = $url;
-    }
-
-    public function getBaseUrl(): string
-    {
-        return $this->baseUrl;
-    }
-
-    /**
-     * @return GridViewColumn[]
-     */
-    public function getSortableColumns(): array
-    {
-        return \array_filter($this->getColumns(), fn($column) => $column->isSortable());
-    }
-
-    /**
-     * @return GridViewColumn[]
-     */
-    public function getFilterableColumns(): array
-    {
-        return \array_filter($this->getColumns(), fn($column) => $column->getFilter() !== null);
-    }
-
-    public function setSortField(string $sortField): void
-    {
-        if (!\in_array($sortField, \array_map(fn($column) => $column->getID(), $this->getSortableColumns()))) {
-            throw new \InvalidArgumentException("Invalid value '{$sortField}' as sort field given.");
-        }
-
-        $this->sortField = $sortField;
-    }
-
-    public function setSortOrder(string $sortOrder): void
-    {
-        if ($sortOrder !== 'ASC' && $sortOrder !== 'DESC') {
-            throw new \InvalidArgumentException("Invalid value '{$sortOrder}' as sort order given.");
-        }
-
-        $this->sortOrder = $sortOrder;
-    }
-
-    public function getSortField(): string
-    {
-        return $this->sortField;
-    }
-
-    public function getSortOrder(): string
-    {
-        return $this->sortOrder;
-    }
-
-    public function getPageNo(): int
-    {
-        return $this->pageNo;
-    }
-
-    public function setPageNo(int $pageNo): void
-    {
-        $this->pageNo = $pageNo;
-    }
-
-    public function getRowsPerPage(): int
-    {
-        return $this->rowsPerPage;
-    }
-
-    public function setRowsPerPage(int $rowsPerPage): void
-    {
-        $this->rowsPerPage = $rowsPerPage;
-    }
-
-    public function isFilterable(): bool
-    {
-        return $this->getFilterableColumns() !== [];
-    }
-
-    public function getFilterActionEndpoint(): string
-    {
-        return LinkHandler::getInstance()->getControllerLink(
-            GridViewFilterAction::class,
-            ['gridView' => \get_class($this)]
-        );
-    }
-
-    public function setActiveFilters(array $filters): void
-    {
-        $this->activeFilters = $filters;
-    }
-
-    public function getActiveFilters(): array
-    {
-        return $this->activeFilters;
-    }
-
-    public function getFilterLabel(string $id): string
-    {
-        $column = $this->getColumn($id);
-        if (!$column) {
-            throw new LogicException("Unknown column '" . $id . "'.");
-        }
-
-        if (!$column->getFilter()) {
-            throw new LogicException("Column '" . $id . "' has no filter.");
-        }
-
-        if (!isset($this->activeFilters[$id])) {
-            throw new LogicException("No value for filter '" . $id . "' found.");
-        }
-
-        return $column->getLabel() . ': ' . $column->getFilter()->renderValue($this->activeFilters[$id]);
-    }
-
-    public function getParameters(): array
-    {
-        return [];
-    }
-
-    public function addRowLink(GridViewRowLink $rowLink): void
-    {
-        $this->rowLink = $rowLink;
-    }
-
-    public function getObjectID(mixed $row): mixed
-    {
-        return '';
-    }
-
-    protected function fireInitializedEvent(): void
-    {
-        $event = $this->getInitializedEvent();
-        if ($event === null) {
-            return;
-        }
-
-        EventHandler::getInstance()->fire($event);
-    }
-
-    protected function getInitializedEvent(): ?IPsr14Event
-    {
-        return null;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/CronjobLogGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/CronjobLogGridView.class.php
deleted file mode 100644 (file)
index 90277a7..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\data\cronjob\Cronjob;
-use wcf\data\cronjob\I18nCronjobList;
-use wcf\data\cronjob\log\CronjobLog;
-use wcf\data\cronjob\log\CronjobLogList;
-use wcf\data\DatabaseObjectList;
-use wcf\event\gridView\CronjobLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\view\grid\filter\SelectFilter;
-use wcf\system\view\grid\filter\TimeFilter;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\NumberColumnRenderer;
-use wcf\system\view\grid\renderer\TimeColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-final class CronjobLogGridView extends DatabaseObjectListGridView
-{
-    public function __construct()
-    {
-        $availableCronjobs = $this->getAvailableCronjobs();
-
-        $this->addColumns([
-            GridViewColumn::for('cronjobLogID')
-                ->label('wcf.global.objectID')
-                ->renderer(new NumberColumnRenderer())
-                ->sortable(),
-            GridViewColumn::for('cronjobID')
-                ->label('wcf.acp.cronjob')
-                ->sortable()
-                ->filter(new SelectFilter($availableCronjobs))
-                ->renderer([
-                    new class($availableCronjobs) extends TitleColumnRenderer {
-                        public function __construct(private readonly array $availableCronjobs) {}
-
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            return $this->availableCronjobs[$value];
-                        }
-                    },
-                ]),
-            GridViewColumn::for('execTime')
-                ->label('wcf.acp.cronjob.log.execTime')
-                ->sortable()
-                ->filter(new TimeFilter())
-                ->renderer(new TimeColumnRenderer()),
-            GridViewColumn::for('success')
-                ->label('wcf.acp.cronjob.log.status')
-                ->sortable()
-                ->filter(new SelectFilter([
-                    1 => 'wcf.acp.cronjob.log.success',
-                    0 => 'wcf.acp.cronjob.log.error',
-                ]))
-                ->renderer([
-                    new class extends DefaultColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            \assert($context instanceof CronjobLog);
-
-                            if ($context->success) {
-                                return '<span class="badge green">' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . '</span>';
-                            }
-                            if ($context->error) {
-                                $label = WCF::getLanguage()->get('wcf.acp.cronjob.log.error');
-                                $buttonId = 'cronjobLogErrorButton' . $context->cronjobLogID;
-                                $id = 'cronjobLogError' . $context->cronjobLogID;
-                                $error = StringUtil::encodeHTML($context->error);
-                                $dialogTitle = StringUtil::encodeJS(WCF::getLanguage()->get('wcf.acp.cronjob.log.error.details'));
-
-                                return <<<HTML
-                                    <button type="button" id="{$buttonId}" class="badge red">
-                                        {$label}
-                                    </button>
-                                    <template id="{$id}">{$error}</template>
-                                    <script data-relocate="true">
-                                        require(['WoltLabSuite/Core/Component/Dialog'], ({ dialogFactory }) => {
-                                            document.getElementById('{$buttonId}').addEventListener('click', () => {
-                                                const dialog = dialogFactory().fromId('{$id}').withoutControls();
-                                                dialog.show('{$dialogTitle}');
-                                            });
-                                        });
-                                    </script>
-                                    HTML;
-                            }
-
-                            return '';
-                        }
-                    },
-                ]),
-        ]);
-
-        $this->setSortField('execTime');
-        $this->setSortOrder('DESC');
-    }
-
-    #[\Override]
-    public function isAccessible(): bool
-    {
-        return WCF::getSession()->getPermission('admin.management.canManageCronjob');
-    }
-
-    #[\Override]
-    protected function createObjectList(): DatabaseObjectList
-    {
-        return new CronjobLogList();
-    }
-
-    #[\Override]
-    protected function getInitializedEvent(): ?IPsr14Event
-    {
-        return new CronjobLogGridViewInitialized($this);
-    }
-
-    private function getAvailableCronjobs(): array
-    {
-        $list = new I18nCronjobList();
-        $list->sqlOrderBy = 'descriptionI18n';
-        $list->readObjects();
-
-        return \array_map(fn(Cronjob $cronjob) => $cronjob->getDescription(), $list->getObjects());
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/DataSourceGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/DataSourceGridView.class.php
deleted file mode 100644 (file)
index b59c065..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use LogicException;
-
-abstract class DataSourceGridView extends AbstractGridView
-{
-    protected array $dataSource;
-
-    public function getRows(): array
-    {
-        $this->sortRows();
-
-        return $this->getRowsForPage();
-    }
-
-    protected function sortRows(): void
-    {
-        $this->getDataSource();
-
-        \uasort($this->dataSource, function (array $a, array $b) {
-            if ($this->getSortOrder() === 'ASC') {
-                return \strcmp($a[$this->getSortField()], $b[$this->getSortField()]);
-            } else {
-                return \strcmp($b[$this->getSortField()], $a[$this->getSortField()]);
-            }
-        });
-    }
-
-    protected function getRowsForPage(): array
-    {
-        return \array_slice($this->getDataSource(), ($this->getPageNo() - 1) * $this->getRowsPerPage(), $this->getRowsPerPage());
-    }
-
-    public function countRows(): int
-    {
-        return \count($this->getDataSource());
-    }
-
-    protected function getDataSource(): array
-    {
-        if (!isset($this->dataSource)) {
-            $this->dataSource = $this->loadDataSource();
-            $this->applyFilters();
-            $this->fireInitializedEvent();
-        }
-
-        return $this->dataSource;
-    }
-
-    protected function applyFilters(): void
-    {
-        foreach ($this->getActiveFilters() as $key => $value) {
-            $column = $this->getColumn($key);
-            if (!$column) {
-                throw new LogicException("Unknown column '" . $key . "'");
-            }
-
-            $this->dataSource = \array_filter($this->dataSource, function (array $row) use ($column, $value) {
-                return $column->getFilter()->matches($value, $row[$column->getID()]);
-            });
-        }
-    }
-
-    protected abstract function loadDataSource(): array;
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php
deleted file mode 100644 (file)
index 7116ac2..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use LogicException;
-use wcf\data\DatabaseObject;
-use wcf\data\DatabaseObjectList;
-
-abstract class DatabaseObjectListGridView extends AbstractGridView
-{
-    protected DatabaseObjectList $objectList;
-    private int $objectCount;
-
-    public function getRows(): array
-    {
-        $this->getObjectList()->readObjects();
-
-        return $this->getObjectList()->getObjects();
-    }
-
-    public function countRows(): int
-    {
-        if (!isset($this->objectCount)) {
-            $this->objectCount = $this->getObjectList()->countObjects();
-        }
-
-        return $this->objectCount;
-    }
-
-    protected function getData(mixed $row, string $identifer): mixed
-    {
-        \assert($row instanceof DatabaseObject);
-
-        return $row->__get($identifer);
-    }
-
-    protected function initObjectList(): void
-    {
-        $this->objectList = $this->createObjectList();
-        $this->objectList->sqlLimit = $this->getRowsPerPage();
-        $this->objectList->sqlOffset = ($this->getPageNo() - 1) * $this->getRowsPerPage();
-        if ($this->getSortField()) {
-            $column = $this->getColumn($this->getSortField());
-            if ($column && $column->getSortById()) {
-                $this->objectList->sqlOrderBy = $column->getSortById() . ' ' . $this->getSortOrder();
-            } else {
-                $this->objectList->sqlOrderBy = $this->getSortField() . ' ' . $this->getSortOrder();
-            }
-        }
-        $this->applyFilters();
-        $this->fireInitializedEvent();
-    }
-
-    public function getObjectList(): DatabaseObjectList
-    {
-        if (!isset($this->objectList)) {
-            $this->initObjectList();
-        }
-
-        return $this->objectList;
-    }
-
-    protected function applyFilters(): void
-    {
-        foreach ($this->getActiveFilters() as $key => $value) {
-            $column = $this->getColumn($key);
-            if (!$column) {
-                throw new LogicException("Unknown column '" . $key . "'");
-            }
-
-            $column->getFilter()->applyFilter($this->getObjectList(), $column->getID(), $value);
-        }
-    }
-
-    #[\Override]
-    public function getObjectID(mixed $row): mixed
-    {
-        \assert($row instanceof DatabaseObject);
-
-        return $row->getObjectID();
-    }
-
-    protected abstract function createObjectList(): DatabaseObjectList;
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/ExceptionLogGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/ExceptionLogGridView.class.php
deleted file mode 100644 (file)
index a075458..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\event\gridView\ExceptionLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\Regex;
-use wcf\system\view\grid\filter\SelectFilter;
-use wcf\system\view\grid\filter\TextFilter;
-use wcf\system\view\grid\renderer\TimeColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\DirectoryUtil;
-use wcf\util\ExceptionLogUtil;
-
-final class ExceptionLogGridView extends DataSourceGridView
-{
-    private array $availableLogFiles;
-
-    public function __construct(bool $applyDefaultFilter = false)
-    {
-        $this->addColumns([
-            GridViewColumn::for('message')
-                ->label('wcf.acp.exceptionLog.exception.message')
-                ->sortable()
-                ->renderer(new TitleColumnRenderer()),
-            GridViewColumn::for('exceptionID')
-                ->label('wcf.acp.exceptionLog.search.exceptionID')
-                ->filter(new TextFilter())
-                ->sortable(),
-            GridViewColumn::for('date')
-                ->label('wcf.acp.exceptionLog.exception.date')
-                ->sortable()
-                ->renderer(new TimeColumnRenderer()),
-            GridViewColumn::for('logFile')
-                ->label('wcf.acp.exceptionLog.search.logFile')
-                ->filter(new SelectFilter($this->getAvailableLogFiles()))
-                ->hidden(true),
-        ]);
-
-        $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
-        $this->setSortField('date');
-        $this->setSortOrder('DESC');
-
-        if ($applyDefaultFilter && $this->getDefaultLogFile() !== null) {
-            $this->setActiveFilters([
-                'logFile' => $this->getDefaultLogFile(),
-            ]);
-        }
-    }
-
-    #[\Override]
-    public function isAccessible(): bool
-    {
-        return WCF::getSession()->getPermission('admin.management.canViewLog');
-    }
-
-    #[\Override]
-    public function getObjectID(mixed $row): mixed
-    {
-        return $row['exceptionID'];
-    }
-
-    #[\Override]
-    protected function loadDataSource(): array
-    {
-        if (!empty($this->getActiveFilters()['exceptionID'])) {
-            $exceptionID = $this->getActiveFilters()['exceptionID'];
-            $contents = $logFile = '';
-            foreach ($this->getAvailableLogFiles() as $logFile) {
-                $contents = \file_get_contents(WCF_DIR . $logFile);
-
-                if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
-                    break;
-                }
-
-                unset($contents);
-            }
-
-            if ($contents === '') {
-                return [];
-            }
-
-            $exceptions = ExceptionLogUtil::splitLog($contents);
-            $parsedExceptions = [];
-
-            foreach ($exceptions as $key => $val) {
-                if ($key !== $exceptionID) {
-                    continue;
-                }
-
-                $parsed = ExceptionLogUtil::parseException($val);
-
-                $parsedExceptions[$key] = [
-                    'exceptionID' => $key,
-                    'message' => $parsed['message'],
-                    'date' => $parsed['date'],
-                    'logFile' => $logFile,
-                ];
-            }
-
-            return $parsedExceptions;
-        } elseif (!empty($this->getActiveFilters()['logFile'])) {
-            $contents = \file_get_contents(WCF_DIR . $this->getActiveFilters()['logFile']);
-            $exceptions = ExceptionLogUtil::splitLog($contents);
-            $parsedExceptions = [];
-
-            foreach ($exceptions as $key => $val) {
-                $parsed = ExceptionLogUtil::parseException($val);
-
-                $parsedExceptions[$key] = [
-                    'exceptionID' => $key,
-                    'message' => $parsed['message'],
-                    'date' => $parsed['date'],
-                    'logFile' => $this->getActiveFilters()['logFile'],
-                ];
-            }
-
-            return $parsedExceptions;
-        }
-
-        return [];
-    }
-
-    #[\Override]
-    protected function applyFilters(): void
-    {
-        // Overwrite the default filtering, as this is already applied when the data is loaded.
-    }
-
-    private function getAvailableLogFiles(): array
-    {
-        if (!isset($this->availableLogFiles)) {
-            $this->availableLogFiles = [];
-            $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
-            $logFiles = DirectoryUtil::getInstance(WCF_DIR . 'log/', false)->getFiles(\SORT_DESC, $fileNameRegex);
-            foreach ($logFiles as $logFile) {
-                $this->availableLogFiles['log/' . $logFile] = 'log/' . $logFile;
-            }
-        }
-
-        return $this->availableLogFiles;
-    }
-
-    private function getDefaultLogFile(): ?string
-    {
-        return \array_key_first($this->getAvailableLogFiles());
-    }
-
-    #[\Override]
-    protected function getInitializedEvent(): ?IPsr14Event
-    {
-        return new ExceptionLogGridViewInitialized($this);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php b/wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php
deleted file mode 100644 (file)
index ea1199d..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\view\grid\filter\IGridViewFilter;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\IColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-
-final class GridViewColumn
-{
-    /**
-     * @var IColumnRenderer[]
-     */
-    private array $renderer = [];
-    private string $label = '';
-    private static DefaultColumnRenderer $defaultRenderer;
-    private bool $sortable = false;
-    private string $sortById = '';
-    private ?IGridViewFilter $filter = null;
-    private bool $hidden = false;
-
-    private function __construct(private readonly string $id) {}
-
-    public static function for(string $id): static
-    {
-        return new static($id);
-    }
-
-    public function render(mixed $value, mixed $context = null): string
-    {
-        if ($this->getRenderers() === []) {
-            return self::getDefaultRenderer()->render($value, $context);
-        }
-
-        foreach ($this->getRenderers() as $renderer) {
-            $value = $renderer->render($value, $context);
-        }
-
-        return $value;
-    }
-
-    public function getClasses(): string
-    {
-        if ($this->getRenderers() === []) {
-            return self::getDefaultRenderer()->getClasses();
-        }
-
-        return \implode(' ', \array_map(
-            static function (IColumnRenderer $renderer) {
-                return $renderer->getClasses();
-            },
-            $this->getRenderers()
-        ));
-    }
-
-    public function renderer(array|IColumnRenderer $renderers): static
-    {
-        if (!\is_array($renderers)) {
-            $renderers = [$renderers];
-        }
-
-        foreach ($renderers as $renderer) {
-            \assert($renderer instanceof IColumnRenderer);
-            $this->renderer[] = $renderer;
-        }
-
-        return $this;
-    }
-
-    public function label(string $languageItem): static
-    {
-        $this->label = WCF::getLanguage()->get($languageItem);
-
-        return $this;
-    }
-
-    public function sortable(bool $sortable = true): static
-    {
-        $this->sortable = $sortable;
-
-        return $this;
-    }
-
-    public function sortById(string $id): static
-    {
-        $this->sortById = $id;
-
-        return $this;
-    }
-
-    /**
-     * @return IColumnRenderer[]
-     */
-    public function getRenderers(): array
-    {
-        return $this->renderer;
-    }
-
-    public function getID(): string
-    {
-        return $this->id;
-    }
-
-    public function getLabel(): string
-    {
-        return $this->label;
-    }
-
-    public function isSortable(): bool
-    {
-        return $this->sortable;
-    }
-
-    public function getSortById(): string
-    {
-        return $this->sortById;
-    }
-
-    public function filter(?IGridViewFilter $filter): static
-    {
-        $this->filter = $filter;
-
-        return $this;
-    }
-
-    public function getFilter(): ?IGridViewFilter
-    {
-        return $this->filter;
-    }
-
-    public function getFilterFormField(): AbstractFormField
-    {
-        if ($this->getFilter() === null) {
-            throw new \LogicException('This column has no filter.');
-        }
-
-        return $this->getFilter()->getFormField($this->getID(), $this->getLabel());
-    }
-
-    private static function getDefaultRenderer(): DefaultColumnRenderer
-    {
-        if (!isset(self::$defaultRenderer)) {
-            self::$defaultRenderer = new DefaultColumnRenderer();
-        }
-
-        return self::$defaultRenderer;
-    }
-
-    public function isTitleColumn(): bool
-    {
-        foreach ($this->getRenderers() as $renderer) {
-            if ($renderer instanceof TitleColumnRenderer) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    public function hidden(bool $hidden = true): static
-    {
-        $this->hidden = $hidden;
-
-        return $this;
-    }
-
-    public function isHidden(): bool
-    {
-        return $this->hidden;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/GridViewRowLink.class.php b/wcfsetup/install/files/lib/system/view/grid/GridViewRowLink.class.php
deleted file mode 100644 (file)
index c6485e5..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\util\StringUtil;
-
-class GridViewRowLink
-{
-    public function __construct(
-        private readonly string $controllerClass = '',
-        private readonly array $parameters = [],
-        private readonly string $cssClass = ''
-    ) {}
-
-    public function render(mixed $value, mixed $context = null, bool $isPrimaryColumn = false): string
-    {
-        $href = '';
-        if ($this->controllerClass) {
-            \assert($context instanceof DatabaseObject);
-            $href = LinkHandler::getInstance()->getControllerLink(
-                $this->controllerClass,
-                \array_merge($this->parameters, ['object' => $context])
-            );
-        }
-
-        $attributes = [];
-        $isButton = true;
-        if ($href) {
-            $attributes[] = 'href="' . $href . '"';
-            $isButton = false;
-        }
-        $attributes[] = 'class="gridView__rowLink ' . StringUtil::encodeHTML($this->cssClass) . '"';
-        $attributes[] = 'tabindex="' . ($isPrimaryColumn ? '0' : '-1') . '"';
-
-        if ($isButton) {
-            return '<button type="button" ' . implode(' ', $attributes) . '>'
-                . $value
-                . '</button>';
-        } else {
-            return '<a ' . implode(' ', $attributes) . '>'
-                . $value
-                . '</a>';
-        }
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/UserOptionGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/UserOptionGridView.class.php
deleted file mode 100644 (file)
index 3c71ea6..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\acp\form\UserOptionEditForm;
-use wcf\data\DatabaseObjectList;
-use wcf\data\user\option\UserOption;
-use wcf\data\user\option\UserOptionList;
-use wcf\event\gridView\UserOptionGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\view\grid\action\DeleteAction;
-use wcf\system\view\grid\action\EditAction;
-use wcf\system\view\grid\action\ToggleAction;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\NumberColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-final class UserOptionGridView extends DatabaseObjectListGridView
-{
-    public function __construct()
-    {
-        $this->addColumns([
-            GridViewColumn::for('optionID')
-                ->label('wcf.global.objectID')
-                ->renderer(new NumberColumnRenderer())
-                ->sortable(),
-            GridViewColumn::for('optionName')
-                ->label('wcf.global.name')
-                ->sortable()
-                ->renderer([
-                    new class extends TitleColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            \assert($context instanceof UserOption);
-
-                            return StringUtil::encodeHTML($context->getTitle());
-                        }
-                    }
-                ]),
-            GridViewColumn::for('categoryName')
-                ->label('wcf.global.category')
-                ->sortable()
-                ->renderer([
-                    new class extends DefaultColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            \assert($context instanceof UserOption);
-
-                            return StringUtil::encodeHTML(
-                                WCF::getLanguage()->get('wcf.user.option.category.' . $context->categoryName)
-                            );
-                        }
-                    }
-                ]),
-            GridViewColumn::for('optionType')
-                ->label('wcf.acp.user.option.optionType')
-                ->sortable(),
-            GridViewColumn::for('showOrder')
-                ->label('wcf.global.showOrder')
-                ->sortable()
-                ->renderer(new NumberColumnRenderer()),
-        ]);
-
-        $this->addActions([
-            new ToggleAction('core/users/options/%s/enable', 'core/users/options/%s/disable'),
-            new EditAction(UserOptionEditForm::class),
-            new DeleteAction('core/users/options/%s', fn(UserOption $row) => $row->canDelete()),
-        ]);
-        $this->addRowLink(new GridViewRowLink(UserOptionEditForm::class));
-        $this->setSortField('showOrder');
-    }
-
-    #[\Override]
-    public function isAccessible(): bool
-    {
-        return WCF::getSession()->getPermission('admin.user.canManageUserOption');
-    }
-
-    #[\Override]
-    protected function createObjectList(): DatabaseObjectList
-    {
-        $list = new UserOptionList();
-        $list->getConditionBuilder()->add(
-            "option_table.categoryName IN (
-                SELECT  categoryName
-                FROM    wcf" . WCF_N . "_user_option_category
-                WHERE   parentCategoryName = ?
-            )",
-            ['profile']
-        );
-
-        return $list;
-    }
-
-    #[\Override]
-    protected function getInitializedEvent(): ?IPsr14Event
-    {
-        return new UserOptionGridViewInitialized($this);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/UserRankGridView.class.php
deleted file mode 100644 (file)
index 73bd487..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\acp\form\UserRankEditForm;
-use wcf\data\DatabaseObjectList;
-use wcf\data\user\group\UserGroup;
-use wcf\data\user\rank\I18nUserRankList;
-use wcf\data\user\rank\UserRank;
-use wcf\event\gridView\UserRankGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\view\grid\action\DeleteAction;
-use wcf\system\view\grid\action\EditAction;
-use wcf\system\view\grid\filter\I18nTextFilter;
-use wcf\system\view\grid\filter\SelectFilter;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\NumberColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-final class UserRankGridView extends DatabaseObjectListGridView
-{
-    public function __construct()
-    {
-        $this->addColumns([
-            GridViewColumn::for('rankID')
-                ->label('wcf.global.objectID')
-                ->renderer(new NumberColumnRenderer())
-                ->sortable(),
-            GridViewColumn::for('rankTitle')
-                ->label('wcf.acp.user.rank.title')
-                ->sortable()
-                ->sortById('rankTitleI18n')
-                ->filter(new I18nTextFilter())
-                ->renderer([
-                    new class extends TitleColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            \assert($context instanceof UserRank);
-
-                            return '<span class="badge label' . ($context->cssClassName ? ' ' . $context->cssClassName : '') . '">'
-                                . StringUtil::encodeHTML($context->getTitle())
-                                . '<span>';
-                        }
-                    }
-                ]),
-            GridViewColumn::for('rankImage')
-                ->label('wcf.acp.user.rank.image')
-                ->sortable()
-                ->renderer([
-                    new class extends DefaultColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            \assert($context instanceof UserRank);
-
-                            return $context->rankImage ? $context->getImage() : '';
-                        }
-                    },
-                ]),
-            GridViewColumn::for('groupID')
-                ->label('wcf.user.group')
-                ->sortable()
-                ->filter(new SelectFilter($this->getAvailableUserGroups()))
-                ->renderer([
-                    new class extends DefaultColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            return StringUtil::encodeHTML(UserGroup::getGroupByID($value)->getName());
-                        }
-                    },
-                ]),
-            GridViewColumn::for('requiredGender')
-                ->label('wcf.user.option.gender')
-                ->sortable()
-                ->renderer([
-                    new class extends DefaultColumnRenderer {
-                        public function render(mixed $value, mixed $context = null): string
-                        {
-                            if (!$value) {
-                                return '';
-                            }
-
-                            return WCF::getLanguage()->get(match ($value) {
-                                1 => 'wcf.user.gender.male',
-                                2 => 'wcf.user.gender.female',
-                                default => 'wcf.user.gender.other'
-                            });
-                        }
-                    },
-                ]),
-            GridViewColumn::for('requiredPoints')
-                ->label('wcf.acp.user.rank.requiredPoints')
-                ->sortable()
-                ->renderer(new NumberColumnRenderer()),
-        ]);
-
-        $this->addActions([
-            new EditAction(UserRankEditForm::class),
-            new DeleteAction('core/users/ranks/%s'),
-        ]);
-        $this->addRowLink(new GridViewRowLink(UserRankEditForm::class));
-        $this->setSortField('rankTitle');
-    }
-
-    #[\Override]
-    public function isAccessible(): bool
-    {
-        return \MODULE_USER_RANK && WCF::getSession()->getPermission('admin.user.rank.canManageRank');
-    }
-
-    #[\Override]
-    protected function createObjectList(): DatabaseObjectList
-    {
-        return new I18nUserRankList();
-    }
-
-    #[\Override]
-    protected function getInitializedEvent(): ?IPsr14Event
-    {
-        return new UserRankGridViewInitialized($this);
-    }
-
-    private function getAvailableUserGroups(): array
-    {
-        $groups = [];
-        foreach (UserGroup::getSortedGroupsByType([], [UserGroup::GUESTS, UserGroup::EVERYONE]) as $group) {
-            $groups[$group->groupID] = $group->getName();
-        }
-
-        return $groups;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/action/AbstractAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/AbstractAction.class.php
deleted file mode 100644 (file)
index 99767ac..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-
-abstract class AbstractAction implements IGridViewAction
-{
-    public function __construct(
-        private readonly ?Closure $isAvailableCallback = null
-    ) {}
-
-    #[\Override]
-    public function isAvailable(mixed $row): bool
-    {
-        if ($this->isAvailableCallback === null) {
-            return true;
-        }
-
-        return ($this->isAvailableCallback)($row);
-    }
-
-    #[\Override]
-    public function isQuickAction(): bool
-    {
-        return false;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/action/DeleteAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/DeleteAction.class.php
deleted file mode 100644 (file)
index b32a998..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\data\ITitledObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class DeleteAction extends AbstractAction
-{
-    public function __construct(
-        private readonly string $endpoint,
-        ?Closure $isAvailableCallback = null
-    ) {
-        parent::__construct($isAvailableCallback);
-    }
-
-    #[\Override]
-    public function render(mixed $row): string
-    {
-        \assert($row instanceof DatabaseObject);
-
-        $label = WCF::getLanguage()->get('wcf.global.button.delete');
-
-        if (!$this->isAvailable($row)) {
-            return <<<HTML
-                <span>
-                    {$label}
-                </span>
-                HTML;
-        }
-
-        $endpoint = StringUtil::encodeHTML(
-            LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
-                \sprintf($this->endpoint, $row->getObjectID())
-        );
-        if ($row instanceof ITitledObject) {
-            $objectName = StringUtil::encodeHTML($row->getTitle());
-        } else {
-            $objectName = '';
-        }
-
-        return <<<HTML
-            <button type="button" data-action="delete" data-object-name="{$objectName}" data-endpoint="{$endpoint}">
-                {$label}
-            </button>
-            HTML;
-    }
-
-    #[\Override]
-    public function renderInitialization(AbstractGridView $gridView): ?string
-    {
-        $id = StringUtil::encodeJS($gridView->getID());
-
-        return <<<HTML
-            <script data-relocate="true">
-                require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => {
-                    setup(document.getElementById('{$id}_table'));
-                });
-            </script>
-            HTML;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/action/EditAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/EditAction.class.php
deleted file mode 100644 (file)
index bf9be5b..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\WCF;
-
-class EditAction extends AbstractAction
-{
-    public function __construct(
-        private readonly string $controllerClass,
-        ?Closure $isAvailableCallback = null
-    ) {
-        parent::__construct($isAvailableCallback);
-    }
-
-    #[\Override]
-    public function render(mixed $row): string
-    {
-        \assert($row instanceof DatabaseObject);
-        $href = LinkHandler::getInstance()->getControllerLink(
-            $this->controllerClass,
-            ['object' => $row]
-        );
-
-        return '<a href="' . $href . '">' . WCF::getLanguage()->get('wcf.global.button.edit') . '</a>';
-    }
-
-    #[\Override]
-    public function renderInitialization(AbstractGridView $gridView): ?string
-    {
-        return null;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/action/IGridViewAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/IGridViewAction.class.php
deleted file mode 100644 (file)
index a3eaf8f..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use wcf\system\view\grid\AbstractGridView;
-
-interface IGridViewAction
-{
-    public function render(mixed $row): string;
-
-    public function renderInitialization(AbstractGridView $gridView): ?string;
-
-    public function isQuickAction(): bool;
-
-    public function isAvailable(mixed $row): bool;
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/action/ToggleAction.class.php b/wcfsetup/install/files/lib/system/view/grid/action/ToggleAction.class.php
deleted file mode 100644 (file)
index 449108b..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class ToggleAction extends AbstractAction
-{
-    public function __construct(
-        private readonly string $enableEndpoint,
-        private readonly string $disableEndpoint,
-        private readonly string $propertyName = 'isDisabled',
-        ?Closure $isAvailableCallback = null
-    ) {
-        parent::__construct($isAvailableCallback);
-    }
-
-    #[\Override]
-    public function render(mixed $row): string
-    {
-        \assert($row instanceof DatabaseObject);
-
-        $enableEndpoint = StringUtil::encodeHTML(
-            LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
-                \sprintf($this->enableEndpoint, $row->getObjectID())
-        );
-        $disableEndpoint = StringUtil::encodeHTML(
-            LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
-                \sprintf($this->disableEndpoint, $row->getObjectID())
-        );
-
-        $ariaLabel = WCF::getLanguage()->get('wcf.global.button.enable');
-        $checked = !$row->{$this->propertyName} ? 'checked' : '';
-
-        return <<<HTML
-            <woltlab-core-toggle-button aria-label="{$ariaLabel}" data-enable-endpoint="{$enableEndpoint}" data-disable-endpoint="{$disableEndpoint}" {$checked}></woltlab-core-toggle-button>
-            HTML;
-    }
-
-    #[\Override]
-    public function renderInitialization(AbstractGridView $gridView): ?string
-    {
-        $id = StringUtil::encodeJS($gridView->getID());
-
-        return <<<HTML
-            <script data-relocate="true">
-                require(['WoltLabSuite/Core/Component/GridView/Action/Toggle'], ({ setup }) => {
-                    setup('{$id}_table');
-                });
-            </script>
-            HTML;
-    }
-
-    #[\Override]
-    public function isQuickAction(): bool
-    {
-        return true;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/I18nTextFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/I18nTextFilter.class.php
deleted file mode 100644 (file)
index 4802d6f..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\WCF;
-
-class I18nTextFilter extends TextFilter
-{
-    #[\Override]
-    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
-    {
-        $list->getConditionBuilder()->add("($id LIKE ? OR $id IN (SELECT languageItem FROM wcf1_language_item WHERE languageID = ? AND languageItemValue LIKE ?))", [
-            '%' . $value . '%',
-            WCF::getLanguage()->languageID,
-            '%' . $value . '%'
-        ]);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/IGridViewFilter.class.php
deleted file mode 100644 (file)
index 4862acb..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-
-interface IGridViewFilter
-{
-    public function getFormField(string $id, string $label): AbstractFormField;
-
-    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void;
-
-    public function matches(string $filterValue, string $rowValue): bool;
-
-    public function renderValue(string $value): string;
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/SelectFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/SelectFilter.class.php
deleted file mode 100644 (file)
index f200071..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\SelectFormField;
-use wcf\system\WCF;
-
-class SelectFilter implements IGridViewFilter
-{
-    public function __construct(private readonly array $options) {}
-
-    #[\Override]
-    public function getFormField(string $id, string $label): AbstractFormField
-    {
-        return SelectFormField::create($id)
-            ->label($label)
-            ->options($this->options);
-    }
-
-    #[\Override]
-    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
-    {
-        $list->getConditionBuilder()->add("$id = ?", [$value]);
-    }
-
-    #[\Override]
-    public function matches(string $filterValue, string $rowValue): bool
-    {
-        return $filterValue === $rowValue;
-    }
-
-    #[\Override]
-    public function renderValue(string $value): string
-    {
-        return WCF::getLanguage()->get($this->options[$value]);
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/TextFilter.class.php
deleted file mode 100644 (file)
index 52605e7..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\TextFormField;
-
-class TextFilter implements IGridViewFilter
-{
-    #[\Override]
-    public function getFormField(string $id, string $label): AbstractFormField
-    {
-        return TextFormField::create($id)
-            ->label($label);
-    }
-
-    #[\Override]
-    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
-    {
-        $list->getConditionBuilder()->add("$id LIKE ?", ['%' . $value . '%']);
-    }
-
-    #[\Override]
-    public function matches(string $filterValue, string $rowValue): bool
-    {
-        return \str_contains(\mb_strtolower($rowValue), \mb_strtolower($filterValue));
-    }
-
-    #[\Override]
-    public function renderValue(string $value): string
-    {
-        return $value;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/filter/TimeFilter.class.php b/wcfsetup/install/files/lib/system/view/grid/filter/TimeFilter.class.php
deleted file mode 100644 (file)
index c366dbd..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\DateRangeFormField;
-use wcf\system\WCF;
-
-class TimeFilter implements IGridViewFilter
-{
-    #[\Override]
-    public function getFormField(string $id, string $label): AbstractFormField
-    {
-        return DateRangeFormField::create($id)
-            ->label($label)
-            ->supportTime();
-    }
-
-    #[\Override]
-    public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
-    {
-        $timestamps = $this->getTimestamps($value);
-
-        if (!$timestamps['from'] && !$timestamps['to']) {
-            return;
-        }
-
-        if (!$timestamps['to']) {
-            $list->getConditionBuilder()->add("$id >= ?", [$timestamps['from']]);
-        } else {
-            $list->getConditionBuilder()->add("$id BETWEEN ? AND ?", [$timestamps['from'], $timestamps['to']]);
-        }
-    }
-
-    #[\Override]
-    public function matches(string $filterValue, string $rowValue): bool
-    {
-        $timestamps = $this->getTimestamps($filterValue);
-
-        if (!$timestamps['from'] && !$timestamps['to']) {
-            return true;
-        }
-
-        if (!$timestamps['to']) {
-            return $rowValue >= $timestamps['from'];
-        } else {
-            return $rowValue >= $timestamps['from'] && $rowValue <= $timestamps['to'];
-        }
-    }
-
-    #[\Override]
-    public function renderValue(string $value): string
-    {
-        $values = explode(';', $value);
-        if (\count($values) !== 2) {
-            return '';
-        }
-
-        $locale = WCF::getLanguage()->getLocale();;
-        $fromString = $toString = '';
-        if ($values[0] !== '') {
-            $fromDateTime = \DateTime::createFromFormat(
-                'Y-m-d\TH:i:sP',
-                $values[0],
-                WCF::getUser()->getTimeZone()
-            );
-            if ($fromDateTime !== false) {
-                $fromString = \IntlDateFormatter::formatObject(
-                    $fromDateTime,
-                    [
-                        \IntlDateFormatter::LONG,
-                        \IntlDateFormatter::SHORT,
-                    ],
-                    $locale
-                );
-            }
-        }
-        if ($values[1] !== '') {
-            $toDateTime = \DateTime::createFromFormat(
-                'Y-m-d\TH:i:sP',
-                $values[1],
-                WCF::getUser()->getTimeZone()
-            );
-            if ($toDateTime !== false) {
-                $toString = \IntlDateFormatter::formatObject(
-                    $toDateTime,
-                    [
-                        \IntlDateFormatter::LONG,
-                        \IntlDateFormatter::SHORT,
-                    ],
-                    $locale
-                );
-            }
-        }
-
-        if ($fromString && $toString) {
-            return $fromString . ' ‐ ' . $toString;
-        } else if ($fromString) {
-            return '>= ' . $fromString;
-        } else if ($toString) {
-            return '<= ' . $toString;
-        }
-
-        return '';
-    }
-
-    private function getTimestamps(string $value): array
-    {
-        $from = 0;
-        $to = 0;
-
-        $values = explode(';', $value);
-        if (\count($values) === 2) {
-            $from = $this->getTimestamp($values[0]);
-            $to = $this->getTimestamp($values[1]);
-        }
-
-        return [
-            'from' => $from,
-            'to' => $to,
-        ];
-    }
-
-    private function getTimestamp(string $date): int
-    {
-        $dateTime = \DateTime::createFromFormat(
-            'Y-m-d\TH:i:sP',
-            $date
-        );
-
-        if ($dateTime !== false) {
-            return $dateTime->getTimestamp();
-        }
-
-        return 0;
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/AbstractColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/AbstractColumnRenderer.class.php
deleted file mode 100644 (file)
index d641d36..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-abstract class AbstractColumnRenderer implements IColumnRenderer
-{
-    public function getClasses(): string
-    {
-        return '';
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/DefaultColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/DefaultColumnRenderer.class.php
deleted file mode 100644 (file)
index 5b510a6..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\util\StringUtil;
-
-class DefaultColumnRenderer extends AbstractColumnRenderer
-{
-    public function render(mixed $value, mixed $context = null): string
-    {
-        return StringUtil::encodeHTML($value);
-    }
-
-    public function getClasses(): string
-    {
-        return 'gridView__column--text';
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/IColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/IColumnRenderer.class.php
deleted file mode 100644 (file)
index 406c492..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-interface IColumnRenderer
-{
-    public function render(mixed $value, mixed $context = null): string;
-
-    public function getClasses(): string;
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/LinkColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/LinkColumnRenderer.class.php
deleted file mode 100644 (file)
index 737b0ba..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\WCF;
-
-class LinkColumnRenderer extends AbstractColumnRenderer
-{
-    public function __construct(
-        private readonly string $controllerClass,
-        private readonly array $parameters = [],
-        private readonly string $titleLanguageItem = ''
-    ) {}
-
-    public function render(mixed $value, mixed $context = null): string
-    {
-        \assert($context instanceof DatabaseObject);
-        $href = LinkHandler::getInstance()->getControllerLink(
-            $this->controllerClass,
-            \array_merge($this->parameters, ['object' => $context])
-        );
-
-        return '<a href="' . $href . '"'
-            . ($this->titleLanguageItem ? ' title="' . WCF::getLanguage()->get($this->titleLanguageItem) . '"' : '') . '>'
-            . $value
-            . '</a>';
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/NumberColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/NumberColumnRenderer.class.php
deleted file mode 100644 (file)
index b14bf7d..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\util\StringUtil;
-
-class NumberColumnRenderer extends AbstractColumnRenderer
-{
-    public function render(mixed $value, mixed $context = null): string
-    {
-        return StringUtil::formatNumeric($value);
-    }
-
-    public function getClasses(): string
-    {
-        return 'gridView__column--digits';
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/PhraseColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/PhraseColumnRenderer.class.php
deleted file mode 100644 (file)
index 604eddb..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class PhraseColumnRenderer extends DefaultColumnRenderer
-{
-    public function render(mixed $value, mixed $context = null): string
-    {
-        return StringUtil::encodeHTML(WCF::getLanguage()->get($value));
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/TimeColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/TimeColumnRenderer.class.php
deleted file mode 100644 (file)
index bbd6941..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\system\WCF;
-
-class TimeColumnRenderer extends AbstractColumnRenderer
-{
-    public function render(mixed $value, mixed $context = null): string
-    {
-        $timestamp = \intval($value);
-        if (!$timestamp) {
-            return '';
-        }
-
-        $dateTime = new \DateTimeImmutable('@' . $timestamp);
-        $dateTime = $dateTime->setTimezone(WCF::getUser()->getTimeZone());
-        $locale = WCF::getLanguage()->getLocale();
-
-        $isFutureDate = $dateTime->getTimestamp() > TIME_NOW;
-
-        $dateAndTime = \IntlDateFormatter::formatObject(
-            $dateTime,
-            [
-                \IntlDateFormatter::LONG,
-                \IntlDateFormatter::SHORT,
-            ],
-            $locale
-        );
-
-        return \sprintf(
-            '<woltlab-core-date-time date="%s"%s>%s</woltlab-core-date-time>',
-            $dateTime->format('c'),
-            $isFutureDate ? ' static' : '',
-            $dateAndTime
-        );
-    }
-
-    public function getClasses(): string
-    {
-        return 'gridView__column--date';
-    }
-}
diff --git a/wcfsetup/install/files/lib/system/view/grid/renderer/TitleColumnRenderer.class.php b/wcfsetup/install/files/lib/system/view/grid/renderer/TitleColumnRenderer.class.php
deleted file mode 100644 (file)
index f8ea52a..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-class TitleColumnRenderer extends DefaultColumnRenderer
-{
-    public function getClasses(): string
-    {
-        return 'gridView__column--title';
-    }
-}