update
This commit is contained in:
6
codex.md
6
codex.md
@@ -186,6 +186,7 @@ Shortcodes:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`[kb_docs]` bindet die Dokumentation in eine normale WordPress-Seite ein.
|
`[kb_docs]` bindet die Dokumentation in eine normale WordPress-Seite ein.
|
||||||
|
Die Ausgabe ist als Doku-App aufgebaut: Startseite rechts, persistente Sidebar links. Die Sidebar zeigt alle Produkte, deren Versionen und fuer die aktive Version alle Seiten, sodass man direkt von der Portalseite in die konkrete Doku springen kann.
|
||||||
|
|
||||||
## 10. Admin-Einstellungen
|
## 10. Admin-Einstellungen
|
||||||
|
|
||||||
@@ -194,10 +195,13 @@ Backend-Menue:
|
|||||||
```text
|
```text
|
||||||
Knowledgebase
|
Knowledgebase
|
||||||
Uebersicht
|
Uebersicht
|
||||||
|
Produkte
|
||||||
Synchronisation
|
Synchronisation
|
||||||
Einstellungen
|
Einstellungen
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Unter `Produkte` koennen importierte Produkte verwaltet werden. Admins koennen Namen und Slugs korrigieren oder ein fehlerhaft importiertes Produkt inklusive der zugehoerigen Doku-Seiten in den Papierkorb verschieben.
|
||||||
|
|
||||||
Einstellungen:
|
Einstellungen:
|
||||||
|
|
||||||
- GitLab Base URL
|
- GitLab Base URL
|
||||||
@@ -239,7 +243,9 @@ Es gibt keine Renderer-Modus-Einstellung mehr. Markdown wird direkt im Plugin ve
|
|||||||
- Bilder aus `images/` werden importiert und im HTML ersetzt.
|
- Bilder aus `images/` werden importiert und im HTML ersetzt.
|
||||||
- Interne `.md`-Links funktionieren.
|
- Interne `.md`-Links funktionieren.
|
||||||
- `/docs/` zeigt die Dokumentationsuebersicht.
|
- `/docs/` zeigt die Dokumentationsuebersicht.
|
||||||
|
- `[kb_docs]` zeigt eine Startseite mit persistenter Produkt-, Versions- und Seitennavigation.
|
||||||
- `/docs/{product}/{version}/` zeigt die Startseite.
|
- `/docs/{product}/{version}/` zeigt die Startseite.
|
||||||
|
- Im Backend koennen Produkte bei fehlerhaften Importen verwaltet und entfernt werden.
|
||||||
- Synchronisation dupliziert unveraenderte Seiten nicht.
|
- Synchronisation dupliziert unveraenderte Seiten nicht.
|
||||||
- Importfehler werden im Backend-Log sichtbar.
|
- Importfehler werden im Backend-Log sichtbar.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -15,6 +15,114 @@
|
|||||||
padding: 24px 20px 48px;
|
padding: 24px 20px 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kb-docs-app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 320px minmax(0, 1fr);
|
||||||
|
gap: 32px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 24px;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--kb-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--kb-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar__brand {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar__brand a {
|
||||||
|
color: var(--kb-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar__search {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar__search .kb-search {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar__search .kb-search h2,
|
||||||
|
.kb-app-sidebar__search .kb-search-results {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-nav,
|
||||||
|
.kb-app-nav ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-top: 1px solid var(--kb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product__link,
|
||||||
|
.kb-app-version-list a,
|
||||||
|
.kb-app-page-list a {
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--kb-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product__link {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-version-list a {
|
||||||
|
padding: 6px 10px 6px 22px;
|
||||||
|
color: var(--kb-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-page-list a {
|
||||||
|
padding: 5px 10px 5px 36px;
|
||||||
|
color: var(--kb-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product__link:hover,
|
||||||
|
.kb-app-version-list a:hover,
|
||||||
|
.kb-app-page-list a:hover,
|
||||||
|
.kb-app-product.is-active > .kb-app-product__link,
|
||||||
|
.kb-app-version-list li.is-active > a,
|
||||||
|
.kb-app-page-list li.is-active > a {
|
||||||
|
background: var(--kb-accent-soft);
|
||||||
|
color: var(--kb-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-docs-home > h1,
|
||||||
|
.kb-docs-product > h1,
|
||||||
|
.kb-docs-version > h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.kb-product-list {
|
.kb-product-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
@@ -195,6 +303,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 780px) {
|
@media (max-width: 780px) {
|
||||||
|
.kb-docs-app {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar {
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
.kb-doc-layout {
|
.kb-doc-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kb-product-card,
|
.kb-product-card,
|
||||||
.kb-search {
|
.kb-search,
|
||||||
|
.kb-app-sidebar {
|
||||||
border-radius: var(--kb-radius);
|
border-radius: var(--kb-radius);
|
||||||
border-color: var(--kb-border);
|
border-color: var(--kb-border);
|
||||||
box-shadow: var(--kb-shadow);
|
box-shadow: var(--kb-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar {
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-sidebar__brand a {
|
||||||
|
font-family: var(--kb-font-strong);
|
||||||
|
color: var(--kb-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product__link,
|
||||||
|
.kb-app-version-list a,
|
||||||
|
.kb-app-page-list a {
|
||||||
|
border-radius: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-app-product__link:hover,
|
||||||
|
.kb-app-version-list a:hover,
|
||||||
|
.kb-app-page-list a:hover,
|
||||||
|
.kb-app-product.is-active > .kb-app-product__link,
|
||||||
|
.kb-app-version-list li.is-active > a,
|
||||||
|
.kb-app-page-list li.is-active > a {
|
||||||
|
background: rgba(0, 167, 230, 0.12);
|
||||||
|
color: var(--kb-primary-shade);
|
||||||
|
}
|
||||||
|
|
||||||
.kb-product-card h2 a {
|
.kb-product-card h2 a {
|
||||||
color: var(--kb-text);
|
color: var(--kb-text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
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
|
public static function shortcodeDocsIndex(): string
|
||||||
{
|
{
|
||||||
return (new TemplateLoader())->capture('documentation-index', [
|
return (new self())->captureRoute('index');
|
||||||
'products' => self::productsWithVersions(),
|
|
||||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
|
||||||
'url_builder' => UrlBuilder::class,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function shortcodeDocsApp(array $atts = []): string
|
public static function shortcodeDocsApp(array $atts = []): string
|
||||||
@@ -188,12 +184,12 @@ final class Router
|
|||||||
$atts = shortcode_atts(['product' => ''], $atts, 'kb_product_index');
|
$atts = shortcode_atts(['product' => ''], $atts, 'kb_product_index');
|
||||||
$router = new self();
|
$router = new self();
|
||||||
|
|
||||||
return $router->captureProduct((string) $atts['product']);
|
return $router->captureRoute('product', sanitize_title((string) $atts['product']));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderIndex(): void
|
private function renderIndex(): void
|
||||||
{
|
{
|
||||||
echo $this->captureIndex();
|
echo $this->captureRoute('index');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function captureIndex(): string
|
private function captureIndex(): string
|
||||||
@@ -207,7 +203,7 @@ final class Router
|
|||||||
|
|
||||||
private function renderProduct(string $productSlug): void
|
private function renderProduct(string $productSlug): void
|
||||||
{
|
{
|
||||||
echo $this->captureProduct($productSlug);
|
echo $this->captureRoute('product', $productSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function captureProduct(string $productSlug): string
|
private function captureProduct(string $productSlug): string
|
||||||
@@ -229,7 +225,7 @@ final class Router
|
|||||||
|
|
||||||
private function renderVersion(string $productSlug, string $versionSlug): void
|
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
|
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
|
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
|
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
|
private function captureRoute(string $route, string $productSlug = '', string $versionSlug = '', string $pageSlug = ''): string
|
||||||
{
|
{
|
||||||
return match ($route) {
|
$content = match ($route) {
|
||||||
'index' => $this->captureIndex(),
|
'index' => $this->captureIndex(),
|
||||||
'product' => $productSlug ? $this->captureProduct($productSlug) : $this->captureIndex(),
|
'product' => $productSlug ? $this->captureProduct($productSlug) : $this->captureIndex(),
|
||||||
'version' => ($productSlug && $versionSlug) ? $this->captureVersion($productSlug, $versionSlug) : $this->captureIndex(),
|
'version' => ($productSlug && $versionSlug) ? $this->captureVersion($productSlug, $versionSlug) : $this->captureIndex(),
|
||||||
'page' => ($productSlug && $versionSlug) ? $this->capturePage($productSlug, $versionSlug, $pageSlug) : $this->captureIndex(),
|
'page' => ($productSlug && $versionSlug) ? $this->capturePage($productSlug, $versionSlug, $pageSlug) : $this->captureIndex(),
|
||||||
default => $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
|
private function render404(): void
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace KbMarkdownImporter;
|
namespace KbMarkdownImporter;
|
||||||
|
|
||||||
use KbMarkdownImporter\Admin\SettingsPage;
|
use KbMarkdownImporter\Admin\SettingsPage;
|
||||||
|
use KbMarkdownImporter\Admin\ProductsPage;
|
||||||
use KbMarkdownImporter\Admin\StatusPage;
|
use KbMarkdownImporter\Admin\StatusPage;
|
||||||
use KbMarkdownImporter\Admin\SyncPage;
|
use KbMarkdownImporter\Admin\SyncPage;
|
||||||
use KbMarkdownImporter\Frontend\Router;
|
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', __('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', __('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']);
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
kb-markdown-importer/templates/docs-app.php
Normal file
58
kb-markdown-importer/templates/docs-app.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
$active_product_slug = (string) ($active_product_slug ?? '');
|
||||||
|
$active_version_slug = (string) ($active_version_slug ?? '');
|
||||||
|
$active_page_slug = (string) ($active_page_slug ?? '');
|
||||||
|
?>
|
||||||
|
<div class="kb-docs-wrap kb-docs-app">
|
||||||
|
<aside class="kb-app-sidebar" aria-label="<?php esc_attr_e('Documentation navigation', 'kb-markdown-importer'); ?>">
|
||||||
|
<div class="kb-app-sidebar__brand">
|
||||||
|
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Knowledgebase', 'kb-markdown-importer'); ?></a>
|
||||||
|
</div>
|
||||||
|
<div class="kb-app-sidebar__search">
|
||||||
|
<?php echo do_shortcode('[kb_search]'); ?>
|
||||||
|
</div>
|
||||||
|
<nav class="kb-app-nav">
|
||||||
|
<?php foreach ((array) $products as $item) : ?>
|
||||||
|
<?php
|
||||||
|
$term = $item['term'];
|
||||||
|
$versions = (array) $item['versions'];
|
||||||
|
$latest = $versions[0] ?? null;
|
||||||
|
$isActiveProduct = $term->slug === $active_product_slug;
|
||||||
|
?>
|
||||||
|
<section class="kb-app-product <?php echo $isActiveProduct ? 'is-active' : ''; ?>">
|
||||||
|
<a class="kb-app-product__link" href="<?php echo esc_url($latest ? $url_builder::version($term->slug, $latest->slug) : $url_builder::product($term->slug)); ?>">
|
||||||
|
<?php echo esc_html($term->name); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ($versions) : ?>
|
||||||
|
<ul class="kb-app-version-list">
|
||||||
|
<?php foreach ($versions as $version) : ?>
|
||||||
|
<?php $isActiveVersion = $isActiveProduct && $version->slug === $active_version_slug; ?>
|
||||||
|
<li class="<?php echo $isActiveVersion ? 'is-active' : ''; ?>">
|
||||||
|
<a href="<?php echo esc_url($url_builder::version($term->slug, $version->slug)); ?>"><?php echo esc_html($version->name); ?></a>
|
||||||
|
<?php if ($isActiveVersion && ! empty($active_pages)) : ?>
|
||||||
|
<ul class="kb-app-page-list">
|
||||||
|
<?php foreach ((array) $active_pages as $page) : ?>
|
||||||
|
<?php
|
||||||
|
$pageSlug = (string) get_post_meta($page->ID, '_kb_page_slug', true);
|
||||||
|
$isActivePage = $pageSlug === $active_page_slug || ('' === $active_page_slug && in_array($pageSlug, ['', 'index'], true));
|
||||||
|
?>
|
||||||
|
<li class="<?php echo $isActivePage ? 'is-active' : ''; ?>">
|
||||||
|
<a href="<?php echo esc_url($url_builder::page($term->slug, $version->slug, $pageSlug)); ?>"><?php echo esc_html(get_the_title($page)); ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<section class="kb-app-main">
|
||||||
|
<?php echo $content; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
?>
|
?>
|
||||||
<main class="kb-docs-wrap">
|
<section class="kb-docs-home">
|
||||||
<h1><?php esc_html_e('Knowledgebase', 'kb-markdown-importer'); ?></h1>
|
<h1><?php esc_html_e('Knowledgebase', 'kb-markdown-importer'); ?></h1>
|
||||||
<?php echo do_shortcode('[kb_search]'); ?>
|
|
||||||
<div class="kb-product-list">
|
<div class="kb-product-list">
|
||||||
<?php foreach ((array) $products as $item) : ?>
|
<?php foreach ((array) $products as $item) : ?>
|
||||||
<?php $term = $item['term']; ?>
|
<?php $term = $item['term']; ?>
|
||||||
|
<?php $latest = $item['versions'][0] ?? null; ?>
|
||||||
<section class="kb-product-card">
|
<section class="kb-product-card">
|
||||||
<h2><a href="<?php echo esc_url($url_builder::product($term->slug)); ?>"><?php echo esc_html($term->name); ?></a></h2>
|
<h2><a href="<?php echo esc_url($latest ? $url_builder::version($term->slug, $latest->slug) : $url_builder::product($term->slug)); ?>"><?php echo esc_html($term->name); ?></a></h2>
|
||||||
<?php if (! empty($item['versions'])) : ?>
|
<?php if (! empty($item['versions'])) : ?>
|
||||||
<ul>
|
<ul>
|
||||||
<?php foreach ($item['versions'] as $version) : ?>
|
<?php foreach ($item['versions'] as $version) : ?>
|
||||||
@@ -19,4 +19,4 @@ defined('ABSPATH') || exit;
|
|||||||
</section>
|
</section>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
|
|||||||
@@ -1,47 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
$render_nav = static function (array $nodes) use (&$render_nav, $base_slug, $product_slug, $version_slug, $url_builder): void {
|
|
||||||
if (! $nodes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '<ul>';
|
|
||||||
foreach ($nodes as $node) {
|
|
||||||
$target = (string) ($node['target'] ?? '');
|
|
||||||
$label = (string) ($node['title'] ?? '');
|
|
||||||
$href = '';
|
|
||||||
|
|
||||||
if ($target) {
|
|
||||||
$slug = preg_replace('/\.md(#.+)?$/', '', basename($target)) ?: basename($target);
|
|
||||||
$slug = in_array(strtolower($slug), ['doku', 'index'], true) ? '' : sanitize_title($slug);
|
|
||||||
$href = $url_builder::page($product_slug, $version_slug, $slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '<li>';
|
|
||||||
if ($href) {
|
|
||||||
printf('<a href="%s">%s</a>', esc_url($href), esc_html($label));
|
|
||||||
} else {
|
|
||||||
echo '<span>' . esc_html($label) . '</span>';
|
|
||||||
}
|
|
||||||
$render_nav((array) ($node['children'] ?? []));
|
|
||||||
echo '</li>';
|
|
||||||
}
|
|
||||||
echo '</ul>';
|
|
||||||
};
|
|
||||||
?>
|
?>
|
||||||
<main class="kb-docs-wrap kb-doc-layout">
|
|
||||||
<aside class="kb-sidebar">
|
|
||||||
<label class="screen-reader-text" for="kb-version-switcher"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label>
|
|
||||||
<select id="kb-version-switcher">
|
|
||||||
<?php foreach ((array) $versions as $item) : ?>
|
|
||||||
<option value="<?php echo esc_attr($item->slug); ?>" data-url="<?php echo esc_url($url_builder::version($product_slug, $item->slug)); ?>" <?php selected($item->slug, $version_slug); ?>><?php echo esc_html($item->name); ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<nav class="kb-sidebar-nav" aria-label="<?php esc_attr_e('Documentation navigation', 'kb-markdown-importer'); ?>">
|
|
||||||
<?php $render_nav((array) $nav_tree); ?>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
<article class="kb-doc-content">
|
<article class="kb-doc-content">
|
||||||
<nav class="kb-breadcrumbs">
|
<nav class="kb-breadcrumbs">
|
||||||
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span>
|
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span>
|
||||||
@@ -56,4 +15,3 @@ $render_nav = static function (array $nodes) use (&$render_nav, $base_slug, $pro
|
|||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
?>
|
?>
|
||||||
<main class="kb-docs-wrap">
|
<section class="kb-docs-product">
|
||||||
<nav class="kb-breadcrumbs"><a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span><?php echo esc_html($product->name); ?></nav>
|
<nav class="kb-breadcrumbs"><a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span><?php echo esc_html($product->name); ?></nav>
|
||||||
<h1><?php echo esc_html($product->name); ?></h1>
|
<h1><?php echo esc_html($product->name); ?></h1>
|
||||||
<h2><?php esc_html_e('Available Versions', 'kb-markdown-importer'); ?></h2>
|
<h2><?php esc_html_e('Available Versions', 'kb-markdown-importer'); ?></h2>
|
||||||
@@ -13,4 +13,4 @@ defined('ABSPATH') || exit;
|
|||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</section>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
?>
|
?>
|
||||||
<main class="kb-docs-wrap">
|
<section class="kb-docs-version">
|
||||||
<nav class="kb-breadcrumbs">
|
<nav class="kb-breadcrumbs">
|
||||||
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span>
|
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span>
|
||||||
<a href="<?php echo esc_url($url_builder::product($product->slug)); ?>"><?php echo esc_html($product->name); ?></a><span>/</span>
|
<a href="<?php echo esc_url($url_builder::product($product->slug)); ?>"><?php echo esc_html($product->name); ?></a><span>/</span>
|
||||||
@@ -14,4 +14,4 @@ defined('ABSPATH') || exit;
|
|||||||
<li><a href="<?php echo esc_url($url_builder::page($product->slug, $version->slug, $slug)); ?>"><?php echo esc_html(get_the_title($page)); ?></a></li>
|
<li><a href="<?php echo esc_url($url_builder::page($product->slug, $version->slug, $slug)); ?>"><?php echo esc_html(get_the_title($page)); ?></a></li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user