update
This commit is contained in:
125
kb-markdown-importer/includes/Admin/ProductsPage.php
Normal file
125
kb-markdown-importer/includes/Admin/ProductsPage.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KbMarkdownImporter\Admin;
|
||||
|
||||
use KbMarkdownImporter\Repository\ProductRepository;
|
||||
|
||||
final class ProductsPage
|
||||
{
|
||||
public static function render(): void
|
||||
{
|
||||
if (! current_user_can('manage_kb_docs')) {
|
||||
wp_die(esc_html__('You do not have permission to manage documentation products.', 'kb-markdown-importer'));
|
||||
}
|
||||
|
||||
$repository = new ProductRepository();
|
||||
self::handleActions($repository);
|
||||
$products = $repository->allWithStats();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Documentation Products', 'kb-markdown-importer'); ?></h1>
|
||||
<p><?php esc_html_e('Manage imported products, repair their URL slugs, or remove a broken import from the frontend.', 'kb-markdown-importer'); ?></p>
|
||||
<?php settings_errors('kb_markdown_products'); ?>
|
||||
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('Product', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Slug', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Versions', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Pages', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Actions', 'kb-markdown-importer'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (! $products) : ?>
|
||||
<tr><td colspan="5"><?php esc_html_e('No products have been imported yet.', 'kb-markdown-importer'); ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($products as $item) : ?>
|
||||
<?php
|
||||
$term = $item['term'];
|
||||
$formId = 'kb-markdown-product-' . (int) $term->term_id;
|
||||
$versions = array_map(static fn (\WP_Term $version): string => $version->name, (array) $item['versions']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<input form="<?php echo esc_attr($formId); ?>" class="regular-text" type="text" name="product_name" value="<?php echo esc_attr($term->name); ?>" aria-label="<?php esc_attr_e('Product name', 'kb-markdown-importer'); ?>">
|
||||
</td>
|
||||
<td>
|
||||
<input form="<?php echo esc_attr($formId); ?>" class="regular-text" type="text" name="product_slug" value="<?php echo esc_attr($term->slug); ?>" aria-label="<?php esc_attr_e('Product slug', 'kb-markdown-importer'); ?>">
|
||||
</td>
|
||||
<td><?php echo esc_html($versions ? implode(', ', $versions) : __('No versions', 'kb-markdown-importer')); ?></td>
|
||||
<td><?php echo esc_html((string) $item['page_count']); ?></td>
|
||||
<td>
|
||||
<form id="<?php echo esc_attr($formId); ?>" method="post" action="">
|
||||
<?php wp_nonce_field('kb_markdown_update_product_' . $term->term_id); ?>
|
||||
<input type="hidden" name="kb_markdown_product_action" value="update">
|
||||
<input type="hidden" name="term_id" value="<?php echo esc_attr((string) $term->term_id); ?>">
|
||||
<?php submit_button(__('Save', 'kb-markdown-importer'), 'secondary small', 'submit', false); ?>
|
||||
</form>
|
||||
<form method="post" action="" style="margin-top:8px;" onsubmit="return window.confirm('<?php echo esc_js(__('Move this product and its imported pages to the trash?', 'kb-markdown-importer')); ?>');">
|
||||
<?php wp_nonce_field('kb_markdown_delete_product_' . $term->term_id); ?>
|
||||
<input type="hidden" name="kb_markdown_product_action" value="delete">
|
||||
<input type="hidden" name="term_id" value="<?php echo esc_attr((string) $term->term_id); ?>">
|
||||
<label>
|
||||
<input type="checkbox" name="trash_pages" value="1" checked>
|
||||
<?php esc_html_e('Trash imported pages', 'kb-markdown-importer'); ?>
|
||||
</label>
|
||||
<?php submit_button(__('Delete product', 'kb-markdown-importer'), 'delete small', 'submit', false); ?>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private static function handleActions(ProductRepository $repository): void
|
||||
{
|
||||
$action = sanitize_key(wp_unslash((string) ($_POST['kb_markdown_product_action'] ?? '')));
|
||||
|
||||
if (! $action) {
|
||||
return;
|
||||
}
|
||||
|
||||
$termId = absint($_POST['term_id'] ?? 0);
|
||||
|
||||
if (! $termId) {
|
||||
add_settings_error('kb_markdown_products', 'missing_term', __('Missing product ID.', 'kb-markdown-importer'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if ('update' === $action) {
|
||||
check_admin_referer('kb_markdown_update_product_' . $termId);
|
||||
$result = $repository->update(
|
||||
$termId,
|
||||
sanitize_text_field(wp_unslash((string) ($_POST['product_name'] ?? ''))),
|
||||
sanitize_title(wp_unslash((string) ($_POST['product_slug'] ?? '')))
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
add_settings_error('kb_markdown_products', 'update_failed', $result->get_error_message(), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
add_settings_error('kb_markdown_products', 'updated', __('Product saved.', 'kb-markdown-importer'), 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
if ('delete' === $action) {
|
||||
check_admin_referer('kb_markdown_delete_product_' . $termId);
|
||||
$trashPages = ! empty($_POST['trash_pages']);
|
||||
$result = $repository->deleteProduct($termId, $trashPages);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
add_settings_error('kb_markdown_products', 'delete_failed', $result->get_error_message(), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
add_settings_error('kb_markdown_products', 'deleted', __('Product deleted.', 'kb-markdown-importer'), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,11 +143,7 @@ final class Router
|
||||
|
||||
public static function shortcodeDocsIndex(): string
|
||||
{
|
||||
return (new TemplateLoader())->capture('documentation-index', [
|
||||
'products' => self::productsWithVersions(),
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
]);
|
||||
return (new self())->captureRoute('index');
|
||||
}
|
||||
|
||||
public static function shortcodeDocsApp(array $atts = []): string
|
||||
@@ -188,12 +184,12 @@ final class Router
|
||||
$atts = shortcode_atts(['product' => ''], $atts, 'kb_product_index');
|
||||
$router = new self();
|
||||
|
||||
return $router->captureProduct((string) $atts['product']);
|
||||
return $router->captureRoute('product', sanitize_title((string) $atts['product']));
|
||||
}
|
||||
|
||||
private function renderIndex(): void
|
||||
{
|
||||
echo $this->captureIndex();
|
||||
echo $this->captureRoute('index');
|
||||
}
|
||||
|
||||
private function captureIndex(): string
|
||||
@@ -207,7 +203,7 @@ final class Router
|
||||
|
||||
private function renderProduct(string $productSlug): void
|
||||
{
|
||||
echo $this->captureProduct($productSlug);
|
||||
echo $this->captureRoute('product', $productSlug);
|
||||
}
|
||||
|
||||
private function captureProduct(string $productSlug): string
|
||||
@@ -229,7 +225,7 @@ final class Router
|
||||
|
||||
private function renderVersion(string $productSlug, string $versionSlug): void
|
||||
{
|
||||
echo $this->captureVersion($productSlug, $versionSlug);
|
||||
echo $this->captureRoute('version', $productSlug, $versionSlug);
|
||||
}
|
||||
|
||||
private function captureVersion(string $productSlug, string $versionSlug): string
|
||||
@@ -257,7 +253,7 @@ final class Router
|
||||
|
||||
private function renderPage(string $productSlug, string $versionSlug, string $pageSlug): void
|
||||
{
|
||||
echo $this->capturePage($productSlug, $versionSlug, $pageSlug);
|
||||
echo $this->captureRoute('page', $productSlug, $versionSlug, $pageSlug);
|
||||
}
|
||||
|
||||
private function capturePage(string $productSlug, string $versionSlug, string $pageSlug): string
|
||||
@@ -301,13 +297,35 @@ final class Router
|
||||
|
||||
private function captureRoute(string $route, string $productSlug = '', string $versionSlug = '', string $pageSlug = ''): string
|
||||
{
|
||||
return match ($route) {
|
||||
$content = match ($route) {
|
||||
'index' => $this->captureIndex(),
|
||||
'product' => $productSlug ? $this->captureProduct($productSlug) : $this->captureIndex(),
|
||||
'version' => ($productSlug && $versionSlug) ? $this->captureVersion($productSlug, $versionSlug) : $this->captureIndex(),
|
||||
'page' => ($productSlug && $versionSlug) ? $this->capturePage($productSlug, $versionSlug, $pageSlug) : $this->captureIndex(),
|
||||
default => $this->captureIndex(),
|
||||
};
|
||||
|
||||
return $this->captureShell($content, $productSlug, $versionSlug, $pageSlug);
|
||||
}
|
||||
|
||||
private function captureShell(string $content, string $productSlug = '', string $versionSlug = '', string $pageSlug = ''): string
|
||||
{
|
||||
$activePages = [];
|
||||
|
||||
if ($productSlug && $versionSlug) {
|
||||
$activePages = $this->pagesForVersion($productSlug, $versionSlug);
|
||||
}
|
||||
|
||||
return (new TemplateLoader())->capture('docs-app', [
|
||||
'content' => $content,
|
||||
'products' => self::productsWithVersions(),
|
||||
'active_product_slug' => $productSlug,
|
||||
'active_version_slug' => $versionSlug,
|
||||
'active_page_slug' => $pageSlug,
|
||||
'active_pages' => $activePages,
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
private function render404(): void
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace KbMarkdownImporter;
|
||||
|
||||
use KbMarkdownImporter\Admin\SettingsPage;
|
||||
use KbMarkdownImporter\Admin\ProductsPage;
|
||||
use KbMarkdownImporter\Admin\StatusPage;
|
||||
use KbMarkdownImporter\Admin\SyncPage;
|
||||
use KbMarkdownImporter\Frontend\Router;
|
||||
@@ -119,6 +120,7 @@ final class Plugin
|
||||
);
|
||||
|
||||
add_submenu_page('kb-markdown-importer', __('Overview', 'kb-markdown-importer'), __('Overview', 'kb-markdown-importer'), 'manage_kb_docs', 'kb-markdown-importer', [StatusPage::class, 'render']);
|
||||
add_submenu_page('kb-markdown-importer', __('Products', 'kb-markdown-importer'), __('Products', 'kb-markdown-importer'), 'manage_kb_docs', 'kb-markdown-products', [ProductsPage::class, 'render']);
|
||||
add_submenu_page('kb-markdown-importer', __('Synchronization', 'kb-markdown-importer'), __('Synchronization', 'kb-markdown-importer'), 'sync_kb_docs', 'kb-markdown-sync', [SyncPage::class, 'render']);
|
||||
add_submenu_page('kb-markdown-importer', __('Settings', 'kb-markdown-importer'), __('Settings', 'kb-markdown-importer'), 'manage_kb_docs', 'kb-markdown-settings', [SettingsPage::class, 'render']);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,117 @@ final class ProductRepository
|
||||
|
||||
return is_wp_error($term) ? 0 : (int) ($term['term_id'] ?? $term);
|
||||
}
|
||||
|
||||
public function allWithStats(): array
|
||||
{
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'kb_product',
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
]);
|
||||
|
||||
if (is_wp_error($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$pageIds = $this->pageIdsForProduct((int) $term->term_id);
|
||||
$versions = [];
|
||||
|
||||
foreach ($pageIds as $pageId) {
|
||||
$pageVersions = wp_get_object_terms($pageId, 'kb_version');
|
||||
|
||||
if (is_wp_error($pageVersions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($pageVersions as $version) {
|
||||
$versions[$version->slug] = $version;
|
||||
}
|
||||
}
|
||||
|
||||
uasort($versions, static fn ($a, $b): int => strnatcasecmp($b->name, $a->name));
|
||||
|
||||
$items[] = [
|
||||
'term' => $term,
|
||||
'page_count' => count($pageIds),
|
||||
'versions' => array_values($versions),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function update(int $termId, string $name, string $slug): \WP_Term|\WP_Error
|
||||
{
|
||||
$name = trim($name);
|
||||
$slug = sanitize_title($slug ?: $name);
|
||||
|
||||
if ('' === $name || '' === $slug) {
|
||||
return new \WP_Error('kb_product_invalid', __('Product name and slug are required.', 'kb-markdown-importer'));
|
||||
}
|
||||
|
||||
$updated = wp_update_term($termId, 'kb_product', [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
|
||||
if (is_wp_error($updated)) {
|
||||
return $updated;
|
||||
}
|
||||
|
||||
foreach ($this->pageIdsForProduct($termId) as $pageId) {
|
||||
update_post_meta($pageId, '_kb_product_slug', $slug);
|
||||
}
|
||||
|
||||
$term = get_term((int) $updated['term_id'], 'kb_product');
|
||||
|
||||
return $term instanceof \WP_Term ? $term : new \WP_Error('kb_product_missing', __('Product could not be loaded after update.', 'kb-markdown-importer'));
|
||||
}
|
||||
|
||||
public function trashProductPages(int $termId): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->pageIdsForProduct($termId, ['publish', 'draft', 'private', 'pending', 'future']) as $pageId) {
|
||||
if (wp_trash_post($pageId)) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function deleteProduct(int $termId, bool $trashPages): true|\WP_Error
|
||||
{
|
||||
if ($trashPages) {
|
||||
$this->trashProductPages($termId);
|
||||
}
|
||||
|
||||
$deleted = wp_delete_term($termId, 'kb_product');
|
||||
|
||||
if (is_wp_error($deleted)) {
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function pageIdsForProduct(int $termId, array $postStatus = ['publish']): array
|
||||
{
|
||||
$query = new \WP_Query([
|
||||
'post_type' => 'kb_doc_page',
|
||||
'post_status' => $postStatus,
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'tax_query' => [
|
||||
['taxonomy' => 'kb_product', 'field' => 'term_id', 'terms' => $termId],
|
||||
],
|
||||
]);
|
||||
|
||||
return array_map('intval', $query->posts);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user