{$view->getPageNo()},
'{unsafe:$view->getBaseUrl()|encodeJS}',
'{unsafe:$view->getSortField()|encodeJS}',
- '{unsafe:$view->getSortOrder()|encodeJS}'
+ '{unsafe:$view->getSortOrder()|encodeJS}',
+ new Map([
+ {foreach from=$view->getParameters() key='name' item='value'}
+ ['{unsafe:$name|encodeJs}', '{unsafe:$value|encodeJs}'],
+ {/foreach}
+ ])
);
});
</script>
{foreach from=$view->getRows() item='row'}
- <tr>
+ <tr data-object-id="{$view->getObjectID($row)}">
{foreach from=$view->getColumns() item='column'}
<td class="{$column->getClasses()}">
{unsafe:$view->renderColumn($column, $row)}
--- /dev/null
+/**
+ * Shows the dialog that shows exception details.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+
+import { renderException } from "WoltLabSuite/Core/Api/Exceptions/RenderException";
+import { copyTextToClipboard } from "WoltLabSuite/Core/Clipboard";
+import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
+import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
+import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { getPhrase } from "WoltLabSuite/Core/Language";
+
+async function showDialog(button: HTMLElement): Promise<void> {
+ const response = await renderException(button.closest("tr")!.dataset.objectId!);
+ if (!response.ok) {
+ return;
+ }
+
+ const dialog = dialogFactory().fromHtml(response.value.template).withoutControls();
+ dialog.content.querySelector(".jsCopyButton")?.addEventListener("click", () => {
+ void copyTextToClipboard(dialog.content.querySelector<HTMLTextAreaElement>(".jsCopyException")!.value);
+ });
+
+ dialog.show(getPhrase("wcf.acp.exceptionLog.exception.message"));
+}
+
+export function setup(): void {
+ wheneverFirstSeen(".jsExceptionLogEntry", (button) => {
+ button.addEventListener(
+ "click",
+ promiseMutex(() => showDialog(button)),
+ );
+ });
+}
sortField: string = "",
sortOrder: string = "ASC",
filters?: Map<string, string>,
+ gridViewParameters?: Map<string, string>,
): Promise<ApiResult<Response>> {
const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`);
url.searchParams.set("gridView", gridViewClass);
url.searchParams.set(`filters[${key}]`, value);
});
}
+ if (gridViewParameters) {
+ gridViewParameters.forEach((value, key) => {
+ url.searchParams.set(`gridViewParameters[${key}]`, value);
+ });
+ }
let response: Response;
try {
#defaultSortField: string;
#defaultSortOrder: string;
#filters: Map<string, string>;
+ #gridViewParameters?: Map<string, string>;
constructor(
gridId: string,
baseUrl: string = "",
sortField = "",
sortOrder = "ASC",
+ gridViewParameters?: Map<string, string>,
) {
this.#gridClassName = gridClassName;
this.#table = document.getElementById(`${gridId}_table`) as HTMLTableElement;
this.#defaultSortField = sortField;
this.#sortOrder = sortOrder;
this.#defaultSortOrder = sortOrder;
+ this.#gridViewParameters = gridViewParameters;
this.#initPagination();
this.#initSorting();
async #loadRows(updateQueryString: boolean = true): Promise<void> {
const response = (
- await getRows(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters)
+ await getRows(
+ this.#gridClassName,
+ this.#pageNo,
+ this.#sortField,
+ this.#sortOrder,
+ this.#filters,
+ this.#gridViewParameters,
+ )
).unwrap();
DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.template);
parameters.push(["sortField", this.#sortField]);
parameters.push(["sortOrder", this.#sortOrder]);
}
- this.#filters.forEach((value, key) => {
- parameters.push([`filters[${key}]`, value]);
- });
+ if (this.#filters) {
+ this.#filters.forEach((value, key) => {
+ parameters.push([`filters[${key}]`, value]);
+ });
+ }
if (parameters.length > 0) {
url.search += url.search !== "" ? "&" : "?";
}
#renderFilters(labels: ArrayLike<string>): void {
+ if (!this.#filterPills) {
+ return;
+ }
this.#filterPills.innerHTML = "";
if (!this.#filters) {
return;
{include file='header' pageTitle='wcf.acp.exceptionLog'}
-<script data-relocate="true">
- $(function() {
- {if $exceptionID}window.location.hash = '{$exceptionID|encodeJS}';{/if}
-
- $('#exceptionID').on('keyup keydown keypress', function () {
- if ($.trim($(this).val()) == '') {
- $('#logFile').enable().parents('dl').removeClass('disabled');
- }
- else {
- $('#logFile').disable().parents('dl').addClass('disabled');
- }
- }).trigger('keypress');
-
- $('.jsCopyException').click(function () {
- $(this).select();
- });
- });
-</script>
<header class="contentHeader">
<div class="contentHeaderTitle">
</form>
{/if}
-{hascontent}
- <div class="paginationTop">
- {content}{pages print=true assign=pagesLinks controller="ExceptionLogView" link="pageNo=%d&logFile=$logFile"}{/content}
- </div>
-{/hascontent}
+{unsafe:$gridView->render()}
-{if !$logFiles|empty}
- {if $logFile}
- {foreach from=$exceptions item='exception' key='exceptionKey'}
- <details id="{$exceptionKey}" class="section exceptionContainer"{if $exception[collapsed]|empty} open{/if}>
- <summary class="sectionTitle">
- {$exception[message]}
- </summary>
-
- <div class="exceptionDetails">
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.date{/lang}</dt>
- <dd>{$exception[date]|plainTime}</dd>
- </dl>
-
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.requestURI{/lang}</dt>
- <dd>{$exception[requestURI]}</dd>
- </dl>
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.referrer{/lang}</dt>
- <dd>{$exception[referrer]}</dd>
- </dl>
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.userAgent{/lang}</dt>
- <dd>{$exception[userAgent]}</dd>
- </dl>
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.memory{/lang}</dt>
- <dd>{$exception[peakMemory]|filesizeBinary} / {if $exception[maxMemory] == -1}∞{else}{$exception[maxMemory]|filesizeBinary}{/if}</dd>
- </dl>
- {foreach from=$exception[chain] item=chain}
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.message{/lang}</dt>
- <dd>{$chain[message]}</dd>
- </dl>
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.class{/lang}</dt>
- <dd>{$chain[class]}</dd>
- </dl>
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.file{/lang}</dt>
- <dd>{$chain[file]} ({$chain[line]})</dd>
- </dl>
- {if !$chain[information]|empty}
- {foreach from=$chain[information] item=extraInformation}
- <dl>
- <dt>{$extraInformation[0]}</dt>
- <dd style="white-space: pre-wrap;">{$extraInformation[1]}</dd>
- </dl>
- {/foreach}
- {/if}
- <dl>
- <dt>{lang}wcf.acp.exceptionLog.exception.stacktrace{/lang}</dt>
- <dd>
- <ol start="0" class="nativeList">
- {foreach from=$chain[stack] item=stack}
- <li>{$stack[file]} ({$stack[line]}): {$stack[class]}{$stack[type]}{$stack[function]}(…)</li>
- {/foreach}
- </ol>
- </dd>
- </dl>
- {/foreach}
- <dl>
- <dt><label for="copyException{$exceptionKey}">{lang}wcf.acp.exceptionLog.exception.copy{/lang}</label></dt>
- <dd><textarea id="copyException{$exceptionKey}" rows="5" cols="40" class="jsCopyException" readonly>{$exception[0]}</textarea></dd>
- </dl>
- </div>
- </details>
- {/foreach}
-
- <footer class="contentFooter">
- {hascontent}
- <div class="paginationBottom">
- {content}{@$pagesLinks}{/content}
- </div>
- {/hascontent}
- </footer>
- {elseif $exceptionID}
- <woltlab-core-notice type="error">{lang}wcf.acp.exceptionLog.exceptionNotFound{/lang}</woltlab-core-notice>
- {/if}
-{else}
- <woltlab-core-notice type="info">{lang}wcf.global.noItems{/lang}</woltlab-core-notice>
-{/if}
+<script data-relocate="true">
+ require(['WoltLabSuite/Core/Acp/Controller/ExceptionLog/View'], ({ setup }) => {
+ {jsphrase name='wcf.acp.exceptionLog.exception.message'}
+ setup();
+ });
+</script>
{include file='footer'}
--- /dev/null
+/**
+ * Shows the dialog that shows exception details.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+define(["require", "exports", "WoltLabSuite/Core/Api/Exceptions/RenderException", "WoltLabSuite/Core/Clipboard", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Language"], function (require, exports, RenderException_1, Clipboard_1, Dialog_1, PromiseMutex_1, Selector_1, Language_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ async function showDialog(button) {
+ const response = await (0, RenderException_1.renderException)(button.closest("tr").dataset.objectId);
+ if (!response.ok) {
+ return;
+ }
+ const dialog = (0, Dialog_1.dialogFactory)().fromHtml(response.value.template).withoutControls();
+ dialog.content.querySelector(".jsCopyButton")?.addEventListener("click", () => {
+ void (0, Clipboard_1.copyTextToClipboard)(dialog.content.querySelector(".jsCopyException").value);
+ });
+ dialog.show((0, Language_1.getPhrase)("wcf.acp.exceptionLog.exception.message"));
+ }
+ function setup() {
+ (0, Selector_1.wheneverFirstSeen)(".jsExceptionLogEntry", (button) => {
+ button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => showDialog(button)));
+ });
+ }
+});
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRows = getRows;
- async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC", filters) {
+ async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC", filters, gridViewParameters) {
const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`);
url.searchParams.set("gridView", gridViewClass);
url.searchParams.set("pageNo", pageNo.toString());
url.searchParams.set(`filters[${key}]`, value);
});
}
+ if (gridViewParameters) {
+ gridViewParameters.forEach((value, key) => {
+ url.searchParams.set(`gridViewParameters[${key}]`, value);
+ });
+ }
let response;
try {
response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().disableLoadingIndicator().fetchAsJson());
#defaultSortField;
#defaultSortOrder;
#filters;
- constructor(gridId, gridClassName, pageNo, baseUrl = "", sortField = "", sortOrder = "ASC") {
+ #gridViewParameters;
+ constructor(gridId, gridClassName, pageNo, baseUrl = "", sortField = "", sortOrder = "ASC", gridViewParameters) {
this.#gridClassName = gridClassName;
this.#table = document.getElementById(`${gridId}_table`);
this.#topPagination = document.getElementById(`${gridId}_topPagination`);
this.#defaultSortField = sortField;
this.#sortOrder = sortOrder;
this.#defaultSortOrder = sortOrder;
+ this.#gridViewParameters = gridViewParameters;
this.#initPagination();
this.#initSorting();
this.#initActions();
void this.#loadRows(updateQueryString);
}
async #loadRows(updateQueryString = true) {
- const response = (await (0, GetRows_1.getRows)(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters)).unwrap();
+ const response = (await (0, GetRows_1.getRows)(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters, this.#gridViewParameters)).unwrap();
Util_1.default.setInnerHtml(this.#table.querySelector("tbody"), response.template);
this.#topPagination.count = response.pages;
this.#bottomPagination.count = response.pages;
parameters.push(["sortField", this.#sortField]);
parameters.push(["sortOrder", this.#sortOrder]);
}
- this.#filters.forEach((value, key) => {
- parameters.push([`filters[${key}]`, value]);
- });
+ if (this.#filters) {
+ this.#filters.forEach((value, key) => {
+ parameters.push([`filters[${key}]`, value]);
+ });
+ }
if (parameters.length > 0) {
url.search += url.search !== "" ? "&" : "?";
url.search += new URLSearchParams(parameters).toString();
}
}
#renderFilters(labels) {
+ if (!this.#filterPills) {
+ return;
+ }
this.#filterPills.innerHTML = "";
if (!this.#filters) {
return;
namespace wcf\acp\page;
+use wcf\page\AbstractGridViewPage;
use wcf\page\AbstractPage;
use wcf\page\MultipleLinkPage;
use wcf\system\event\EventHandler;
use wcf\system\Regex;
use wcf\system\registry\RegistryHandler;
use wcf\system\request\LinkHandler;
+use wcf\system\view\grid\AbstractGridView;
+use wcf\system\view\grid\ExceptionLogGridView;
use wcf\system\WCF;
use wcf\util\DirectoryUtil;
use wcf\util\ExceptionLogUtil;
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/
-class ExceptionLogViewPage extends MultipleLinkPage
+class ExceptionLogViewPage extends AbstractGridViewPage
{
/**
* @inheritDoc
*/
public $neededPermissions = ['admin.management.canViewLog'];
- /**
- * @inheritDoc
- */
- public $itemsPerPage = 10;
-
- /**
- * given exceptionID
- * @var string
- */
- public $exceptionID = '';
-
/**
* @inheritDoc
*/
public $forceCanonicalURL = true;
- /**
- * active logfile
- * @var string
- */
- public $logFile = '';
+ public string $exceptionID = '';
+ public string $logFile = '';
/**
* available logfiles
* @var string[]
*/
- public $logFiles = [];
+ public array $logFiles = [];
- /**
- * exceptions shown
- * @var array
- */
- public $exceptions = [];
-
- /**
- * @inheritDoc
- */
+ #[\Override]
public function readParameters()
{
parent::readParameters();
$this->canonicalURL = LinkHandler::getInstance()->getControllerLink(self::class, $parameters);
}
- /**
- * @inheritDoc
- */
+ #[\Override]
public function readData()
{
- AbstractPage::readData();
+ $this->markNotificationsAsRead();
+ $this->readLogFiles();
+ $this->validateParameters();
+
+ parent::readData();
+ }
- // mark notifications as read
+ private function markNotificationsAsRead(): void
+ {
RegistryHandler::getInstance()->set('com.woltlab.wcf', 'exceptionMailerTimestamp', TIME_NOW);
+ }
+ private function readLogFiles(): void
+ {
$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) {
$pathname = WCF_DIR . 'log/' . $logFile;
$this->logFiles[$pathname] = $pathname;
}
+ }
+
+ private function validateParameters(): void
+ {
+ $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
+ if ($this->exceptionID) {
+ // search the appropriate file
+ foreach ($this->logFiles as $logFile) {
+ $contents = \file_get_contents($logFile);
+
+ if (\str_contains($contents, '<<<<<<<<' . $this->exceptionID . '<<<<')) {
+ $fileNameRegex->match($logFile);
+ $matches = $fileNameRegex->getMatches();
+ $this->logFile = $matches[0];
+ break;
+ }
+
+ unset($contents);
+ }
+
+ if (!isset($contents)) {
+ $this->logFile = '';
+
+ return;
+ }
+ } elseif ($this->logFile) {
+ if (!$fileNameRegex->match(\basename($this->logFile))) {
+ throw new IllegalLinkException();
+ }
+ if (!\file_exists(WCF_DIR . 'log/' . $this->logFile)) {
+ throw new IllegalLinkException();
+ }
+ }
+ }
+
+ #[\Override]
+ protected function createGridViewController(): AbstractGridView
+ {
+ return new ExceptionLogGridView($this->logFile, $this->exceptionID);
+ }
+
+ #[\Override]
+ protected function initGridView(): void
+ {
+ parent::initGridView();
+
+ $parameters = [];
+ if ($this->exceptionID !== '') {
+ $parameters['exceptionID'] = $this->exceptionID;
+ } elseif ($this->logFile !== '') {
+ $parameters['logFile'] = $this->logFile;
+ }
+
+ $this->gridView->setBaseUrl(LinkHandler::getInstance()->getControllerLink(static::class, $parameters));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ /*public function readData()
+ {
+ AbstractPage::readData();
if ($this->exceptionID) {
// search the appropriate file
unset($this->exceptions[$key]);
}
}
- }
+ }*/
/**
* @inheritDoc
*/
- public function countItems()
+ /*public function countItems()
{
// call countItems event
EventHandler::getInstance()->fireAction($this, 'countItems');
return \count($this->exceptions);
- }
+ }*/
/**
* Switches to the page containing the exception with the given ID.
*
* @param string $exceptionID
*/
- public function searchPage($exceptionID)
+ /*public function searchPage($exceptionID)
{
$i = 1;
}
$this->pageNo = \ceil($i / $this->itemsPerPage);
- }
+ }*/
- /**
- * @inheritDoc
- */
+ #[\Override]
public function assignVariables()
{
parent::assignVariables();
'exceptionID' => $this->exceptionID,
'logFiles' => \array_flip(\array_map('basename', $this->logFiles)),
'logFile' => $this->logFile,
- 'exceptions' => $this->exceptions,
]);
}
}
if ($this->filters !== []) {
$this->gridView->setActiveFilters($this->filters);
}
- $this->gridView->setPageNo($this->pageNo);
+ if ($this->pageNo != 1) {
+ $this->gridView->setPageNo($this->pageNo);
+ }
$this->gridView->setBaseUrl(LinkHandler::getInstance()->getControllerLink(static::class));
}
throw new UserInputException('gridView', 'invalid');
}
- $view = new $parameters->gridView();
+ $view = new $parameters->gridView(...$parameters->gridViewParameters);
\assert($view instanceof AbstractGridView);
if (!$view->isAccessible()) {
public readonly string $sortField,
public readonly string $sortOrder,
/** @var string[] */
- public readonly array $filters
+ public readonly array $filters,
+ /** @var string[] */
+ public readonly array $gridViewParameters,
) {}
}
--- /dev/null
+<?php
+
+namespace wcf\system\view\grid;
+
+abstract class ArrayGridView extends AbstractGridView
+{
+ protected array $dataArray = [];
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->dataArray = $this->getDataArray();
+ }
+
+ public function getRows(): array
+ {
+ $this->sortRows();
+
+ return $this->getRowsForPage();
+ }
+
+ protected function sortRows(): void
+ {
+ \uasort($this->dataArray, 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->dataArray, ($this->getPageNo() - 1) * $this->getRowsPerPage(), $this->getRowsPerPage());
+ }
+
+ public function countRows(): int
+ {
+ return \count($this->dataArray);
+ }
+
+ protected abstract function getDataArray(): array;
+}
}
}
+ #[\Override]
+ public function getObjectID(mixed $row): mixed
+ {
+ \assert($row instanceof DatabaseObject);
+
+ return $row->getObjectID();
+ }
+
protected abstract function createObjectList(): DatabaseObjectList;
}
--- /dev/null
+<?php
+
+namespace wcf\system\view\grid;
+
+use wcf\system\view\grid\renderer\TimeColumnRenderer;
+use wcf\system\view\grid\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\ExceptionLogUtil;
+
+final class ExceptionLogGridView extends ArrayGridView
+{
+ public function __construct(
+ private readonly string $logFile,
+ private readonly string $exceptionID = ''
+ ) {
+ parent::__construct();
+
+ if ($this->exceptionID) {
+ $this->sortRows();
+ $this->jumpToException();
+ }
+ }
+
+ #[\Override]
+ protected function init(): void
+ {
+ $this->addColumns([
+ GridViewColumn::for('message')
+ ->label('wcf.acp.exceptionLog.exception.message')
+ ->sortable()
+ ->renderer(new TitleColumnRenderer()),
+ GridViewColumn::for('exceptionID')
+ ->label('wcf.global.objectID')
+ ->sortable(),
+ GridViewColumn::for('date')
+ ->label('wcf.acp.exceptionLog.exception.date')
+ ->sortable()
+ ->renderer(new TimeColumnRenderer()),
+ ]);
+
+ $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
+ $this->setSortField('date');
+ $this->setSortOrder('DESC');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canViewLog');
+ }
+
+ private function jumpToException(): void
+ {
+ $i = 1;
+ foreach ($this->dataArray as $key => $val) {
+ if ($key == $this->exceptionID) {
+ break;
+ }
+ $i++;
+ }
+
+ $this->setPageNo(\ceil($i / $this->getRowsPerPage()));
+ }
+
+ #[\Override]
+ public function getParameters(): array
+ {
+ return ['logFile' => $this->logFile];
+ }
+
+ #[\Override]
+ public function getObjectID(mixed $row): mixed
+ {
+ return $row['exceptionID'];
+ }
+
+ protected function getDataArray(): array
+ {
+ if (!$this->logFile) {
+ return [];
+ }
+
+ $contents = \file_get_contents(WCF_DIR . 'log/' . $this->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'],
+ ];
+ }
+
+ return $parsedExceptions;
+ }
+}
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
return self::$defaultRenderer;
}
+
+ public function isTitleColumn(): bool
+ {
+ foreach ($this->getRenderers() as $renderer) {
+ if ($renderer instanceof TitleColumnRenderer) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}