PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / admin / AdminTableColumnHeaders.php

AdminTableColumnHeaders.php in 404 Solution 4.3.0, at includes/admin/AdminTableColumnHeaders.php

222 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Renders shared admin list-table column header rows.
9 */
10 class ABJ_404_Solution_AdminTableColumnHeaders {
11
12 /** @var ABJ_404_Solution_Functions */
13 private $f;
14 /** @var ABJ_404_Solution_PluginLogic */
15 private $logic;
16 /** @var ABJ_404_Solution_View_Shared */
17 private $shared;
18 /** @var ABJ_404_Solution_ViewReadServiceInterface|null Sort-key backfill readiness, for the captured-tab pending-sort headers. */
19 private $viewReadService;
20
21 /**
22 * @param ABJ_404_Solution_Functions $functions
23 * @param ABJ_404_Solution_PluginLogic $pluginLogic
24 * @param ABJ_404_Solution_View_Shared $shared
25 * @param ABJ_404_Solution_ViewReadServiceInterface|null $viewReadService Optional;
26 * when supplied, the URL / Destination headers on the captured tab are
27 * rendered non-sortable with a progress tooltip while their narrow sort-key
28 * backfill is still running (the post-upgrade window). Null in contexts that
29 * never reach that state (e.g. the logs table).
30 */
31 public function __construct(ABJ_404_Solution_Functions $functions,
32 ABJ_404_Solution_PluginLogic $pluginLogic, ABJ_404_Solution_View_Shared $shared,
33 $viewReadService = null) {
34 $this->f = $functions;
35 $this->logic = $pluginLogic;
36 $this->shared = $shared;
37 $this->viewReadService = $viewReadService;
38 }
39
40 /**
41 * @param string $sub
42 * @param array<string, array<string, string>> $columns
43 */
44 public function render(string $sub, array $columns): string {
45 $tableOptions = $this->logic->settingsUpdate()->getTableOptions($sub);
46 $selectAllTh = $this->renderSelectAllHeader($sub);
47 $columnThs = '';
48 foreach ($columns as $column) {
49 $columnThs .= $this->renderColumnHeader($sub, $tableOptions, $column);
50 }
51
52 return $this->f->str_replace(
53 array('{select_all_th}', '{column_ths}'),
54 array($selectAllTh, $columnThs),
55 $this->tpl('viewLogsColumnsHeaderRow.html')
56 );
57 }
58
59 private function renderSelectAllHeader(string $sub): string {
60 $cbinfoStyle = 'vertical-align: middle; padding-bottom: 4px;';
61 if ($sub == 'abj404_logs') {
62 $cbinfoStyle .= ' width: 0px;';
63 }
64 $selectAllCheckbox = '';
65 if ($sub != 'abj404_logs') {
66 $selectAllCheckbox = $this->f->str_replace(
67 '{select_all_label}',
68 esc_attr__('Select all', '404-solution'),
69 $this->tpl('viewLogsColumnsSelectAllCheckbox.html')
70 );
71 }
72
73 return $this->f->str_replace(
74 array('{cb_info_style}', '{select_all_checkbox}'),
75 array($cbinfoStyle, $selectAllCheckbox),
76 $this->tpl('viewLogsColumnsSelectAllTh.html')
77 );
78 }
79
80 /**
81 * @param array<string, mixed> $tableOptions
82 * @param array<string, string> $column
83 */
84 private function renderColumnHeader(string $sub, array $tableOptions, array $column): string {
85 $style = '';
86 if (isset($column['width']) && $column['width'] != '') {
87 $style = ' style="width: ' . esc_attr($column['width']) . ';" ';
88 }
89
90 $orderby = isset($column['orderby']) ? $column['orderby'] : '';
91 $sortState = $this->shared->getHeaderSortState(
92 $tableOptions,
93 (string)$orderby,
94 ($orderby == 'timestamp' || $orderby == 'last_used' || $orderby == 'logshits')
95 );
96
97 // Captured tab, post-upgrade window: the URL / Destination sorts cannot be
98 // served index-ordered until their narrow sort-key backfill converges, and
99 // ordering by the wide source column would filesort the captured majority
100 // (a max_statement_time risk on large sites). Render the header
101 // non-sortable with a progress tooltip instead of offering a sort that
102 // would hang or silently fall back. Self-heals: once the latch flips the
103 // header becomes sortable again with no user action.
104 $pendingTooltip = $this->pendingSortTooltip($sub, (string)$orderby);
105 if ($pendingTooltip !== '') {
106 $sortState['isSortable'] = false;
107 $column['title_attr'] = $pendingTooltip;
108 $column['pending_sort_orderby'] = (string)$orderby;
109 unset($column['title_attr_html']);
110 }
111
112 $thClass = $sortState['isSortable'] ? ' ' . $sortState['thClass'] : '';
113 if (isset($column['class']) && $column['class'] != '') {
114 $thClass .= ' ' . esc_attr($column['class']);
115 }
116
117 $titleContent = $this->titleContent($sub, $tableOptions, $column, $sortState);
118 return $this->f->str_replace(
119 array('{style_attr}', '{extra_class}', '{title_content}', '{tooltip_html}'),
120 array($style, $thClass, $titleContent, $this->tooltipHtml($column)),
121 $this->tpl('viewLogsColumnsHeaderTh.html')
122 );
123 }
124
125 /**
126 * @param array<string, mixed> $tableOptions
127 * @param array<string, string> $column
128 * @param array<string, mixed> $sortState
129 */
130 private function titleContent(string $sub, array $tableOptions, array $column, array $sortState): string {
131 $title = isset($column['title']) ? $column['title'] : '';
132 if (!$sortState['isSortable']) {
133 return $title;
134 }
135
136 $orderby = isset($column['orderby']) ? $column['orderby'] : '';
137 $url = '?page=' . ABJ404_PP;
138 if ($sub !== '') {
139 $url .= '&subpage=' . rawurlencode((string)$sub);
140 }
141 if ($sub == 'abj404_logs') {
142 $rawLogsId = $tableOptions['logsid'] ?? 0;
143 $url .= '&id=' . (is_scalar($rawLogsId) ? (string)$rawLogsId : '0');
144 }
145 if (($tableOptions['filter'] ?? 0) != 0) {
146 $rawFilter = $tableOptions['filter'] ?? 0;
147 $url .= '&filter=' . (is_scalar($rawFilter) ? (string)$rawFilter : '0');
148 }
149 $rawFilterText = $tableOptions['filterText'] ?? '';
150 if (is_scalar($rawFilterText) && (string)$rawFilterText !== '') {
151 $url .= '&filterText=' . rawurlencode((string)$rawFilterText);
152 }
153 $rawNextOrder = $sortState['nextOrder'] ?? '';
154 $nextOrder = is_scalar($rawNextOrder) ? (string)$rawNextOrder : '';
155 $url .= '&orderby=' . $orderby . '&order=' . $nextOrder;
156
157 return $this->f->str_replace(
158 array('{url}', '{orderby}', '{title}', '{sort_indicator}'),
159 array(esc_url($url), (string)$orderby, esc_html($title), $sortState['indicator']),
160 $this->tpl('viewLogsColumnsHeaderLink.html')
161 );
162 }
163
164 /**
165 * The hover-tooltip text for a URL / Destination header on a redirects-table
166 * tab whose narrow sort key cannot yet be served index-ordered, or '' when the
167 * sort is available, this is the Logs tab (a different table, not backed by the
168 * redirects sort keys), or no readiness service was supplied. Delegates the
169 * readiness + percentage + message to View_Shared::pendingSortTooltipText so
170 * the captured-tab renderer (View_CapturedURLsTable) shares the same source.
171 * See ViewQueryBuilder::resolveEffectiveSort for the matching query-side
172 * substitution (the list shows newest-first meanwhile).
173 *
174 * @param string $sub
175 * @param string $orderby
176 * @return string
177 */
178 private function pendingSortTooltip(string $sub, string $orderby): string {
179 // The narrow sort keys back the redirects table (Page Redirects + Captured
180 // tabs). The Logs tab sorts a different table, so its url/dest columns are
181 // never gated by sort-key readiness. (This renderer serves Page Redirects
182 // and Logs; the captured tab uses View_CapturedURLsTable.)
183 if ($sub === 'abj404_logs') {
184 return '';
185 }
186 return $this->shared->pendingSortTooltipText($orderby, $this->viewReadService);
187 }
188
189 /** @param array<string, string> $column */
190 private function tooltipHtml(array $column): string {
191 if (array_key_exists('title_attr_html', $column) && !empty($column['title_attr_html'])) {
192 return $this->f->str_replace(
193 array('{more_info_label}', '{tooltip_body}'),
194 array(esc_attr__('More info', '404-solution'), (string)$column['title_attr_html']),
195 $this->tpl('viewLogsColumnsHeaderTooltip.html')
196 ) . "\n";
197 }
198 if (array_key_exists('title_attr', $column) && !empty($column['title_attr'])) {
199 $tooltipHtml = $this->f->str_replace(
200 array('{more_info_label}', '{tooltip_body}'),
201 array(esc_attr__('More info', '404-solution'), esc_html($column['title_attr'])),
202 $this->tpl('viewLogsColumnsHeaderTooltip.html')
203 );
204 if (array_key_exists('pending_sort_orderby', $column) && is_scalar($column['pending_sort_orderby'])) {
205 $tooltipHtml = str_replace(
206 'class="abj404-header-tooltip lefty-tooltip"',
207 'class="abj404-header-tooltip lefty-tooltip" data-abj404-pending-sort="' . esc_attr((string)$column['pending_sort_orderby']) . '"',
208 $tooltipHtml
209 );
210 }
211 return $tooltipHtml . "\n";
212 }
213
214 return '';
215 }
216
217 private function tpl(string $name): string {
218 $raw = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . '/../html/' . $name, false);
219 return rtrim((string)$raw, "\n");
220 }
221 }
222