fix(admin): 修复销售数据排序图标错位与快捷编辑控件被剥成文本

P1 排序图标跑到「销售数据」文字前面:
原因是注册了 manage_edit-post_sortable_columns。注册后 WordPress 会给
该列套上自己的排序指示器与 <a>,而列头 label 里已经自带一个 <a>,
浏览器解析嵌套 <a> 时把内层内容抛出,箭头因而显示到文字之前。
参考主题不注册该钩子,靠 label 链接 + pre_get_posts 落地排序,现改为
同一做法,同时删掉因此变成死代码的钩子与方法。

P2 快捷/批量编辑新增的配置项不可用,只显示为纯文本:
render_edit_boxes() 用 wp_kses_post() 包裹面板输出。该函数按「文章
内容」白名单过滤,select/option/input/fieldset/legend/label 均不在
白名单内,标签被剥掉只剩文字,于是所有控件退化成纯文本。改为原样
输出,字段值在生成时已逐个 esc_attr/esc_html 转义。

同步更新两份契约测试:sales 列表头排序改为断言「不注册 sortable」,
并为快捷编辑新增一条防回归断言,锁住不得再用 wp_kses_post 包裹面板。

验证:node --test tests/widget-system/*.test.cjs 444 项全部通过
(原有 443 项 + 新增 1 项);php -l 无语法错误。
This commit is contained in:
2026-10-06 19:19:08 +08:00
parent ef718042d2
commit 27868b6131
3 changed files with 21 additions and 18 deletions
+3 -15
View File
@@ -38,7 +38,6 @@ class PostListEnhance {
public function init(): void {
add_filter('manage_post_posts_columns', [$this, 'register_columns']);
add_action('manage_post_posts_custom_column', [$this, 'render_column'], 10, 2);
add_filter('manage_edit-post_sortable_columns', [$this, 'register_sortable_columns']);
add_action('restrict_manage_posts', [$this, 'render_type_filter']);
add_action('pre_get_posts', [$this, 'apply_query_args'], 99);
@@ -108,19 +107,6 @@ class PostListEnhance {
return $new_columns;
}
/**
* @param array<string,string> $columns
* @return array<string,string>
*/
public function register_sortable_columns($columns) {
if (!is_array($columns)) {
return $columns;
}
$columns[self::COLUMN_STATS] = 'views';
$columns[self::COLUMN_SALES] = 'sales_volume';
return $columns;
}
/**
* @param string $column
* @param int $post_id
@@ -305,7 +291,9 @@ class PostListEnhance {
return;
}
echo wp_kses_post($this->edit_boxes_html());
// 表单控件(select/input/fieldset)不在 wp_kses_post 的文章白名单里,
// 会被剥成纯文本,因此这里直接输出;字段值均已 esc_attr/esc_html 转义。
echo $this->edit_boxes_html();
}
private function edit_boxes_html(): string {
@@ -24,9 +24,12 @@ test('admin post list has the sales data column with a sortable header', () => {
assert.match(list, /'aksrt_sales'/)
assert.match(list, /orderby.*sales_volume/)
// 排序列必须在表头注册,并把 sales_volume 映射回真实存储键。
assert.match(list, /manage_edit-post_sortable_columns/)
assert.match(list, /sales_volume/)
// 排序靠表头 label 里的链接 + pre_get_posts 落地,不注册 sortable_columns:
// 注册后 WordPress 会套自己的排序指示器与 <a>,与 label 里的 <a> 嵌套,
// 箭头会被解析到文字前面。
assert.doesNotMatch(list, /manage_edit-post_sortable_columns/)
assert.match(list, /pre_get_posts/)
assert.match(list, /'sales_volume'/)
assert.match(list, /RESOURCE_SALES_VOLUME_KEY/)
})
@@ -44,6 +44,18 @@ test('quick-edit inputs are namespaced under aksrt_bulk_edit', () => {
assert.match(list, /\[operation\]/)
assert.match(list, /\[val\]/)
})
test('quick-edit markup is emitted without kses stripping the controls', () => {
// select/option/input/fieldset/legend 不在 wp_kses_post 的文章白名单里,
// 一旦用它包上面板,所有控件会被剥成纯文本(表现为「新增的操作都不可用」)。
// 字段值均已在生成时 esc_attr/esc_html 转义,这里必须原样输出。
assert.doesNotMatch(list, /wp_kses_post\(\$this->edit_boxes_html\(\)\)/)
// 面板必须真的产生表单控件,而不是纯文本。
assert.match(list, /<select name=/)
assert.match(list, /<input type="text" name=/)
assert.match(list, /<fieldset/)
assert.match(list, /edit_boxes_html\(\)/)
})
test('number fields support the full operation set', () => {
for (const op of ['ignore', 'set', 'plus', 'subtract', 'multiply', 'division']) {
assert.match(list, new RegExp(`value="${op}"`), `${op} operation exists`)