new file: olm-login.php
This commit is contained in:
@@ -10,33 +10,40 @@
|
||||
}
|
||||
|
||||
.kb-docs-wrap {
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 20px 48px;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
width: calc(100vw - 16px);
|
||||
max-width: none !important;
|
||||
margin: 0 0 0 calc(-50vw + 8px);
|
||||
padding: 20px clamp(14px, 2.8vw, 44px) 56px;
|
||||
box-sizing: border-box;
|
||||
background: color-mix(in srgb, var(--kb-surface-muted) 72%, #ffffff);
|
||||
}
|
||||
|
||||
.kb-docs-app {
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
gap: 32px;
|
||||
grid-template-columns: minmax(270px, 320px) minmax(0, 1fr);
|
||||
gap: clamp(20px, 2.2vw, 34px);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.kb-app-sidebar {
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
max-height: calc(100vh - 48px);
|
||||
top: 16px;
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--kb-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
padding: 14px;
|
||||
background: var(--kb-surface);
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.kb-app-sidebar__brand {
|
||||
margin-bottom: 14px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 2px;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.kb-app-sidebar__brand a {
|
||||
@@ -60,6 +67,27 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kb-app-active-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
margin: 0 -4px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--kb-accent) 28%, var(--kb-border));
|
||||
border-left: 4px solid var(--kb-accent);
|
||||
border-radius: 8px;
|
||||
padding: 11px 10px 12px;
|
||||
background: var(--kb-surface);
|
||||
box-shadow: 0 10px 24px rgba(16, 24, 40, 0.1);
|
||||
}
|
||||
|
||||
.kb-app-active-nav__title {
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
color: var(--kb-text);
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kb-app-nav,
|
||||
.kb-app-nav ul {
|
||||
margin: 0;
|
||||
@@ -67,11 +95,32 @@
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kb-app-product {
|
||||
padding: 10px 0;
|
||||
.kb-app-nav__title {
|
||||
margin: 2px 0 8px;
|
||||
padding: 0 2px;
|
||||
color: var(--kb-text);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.kb-app-category {
|
||||
padding: 9px 0;
|
||||
border-top: 1px solid var(--kb-border);
|
||||
}
|
||||
|
||||
.kb-app-category h3 {
|
||||
margin: 0 0 6px;
|
||||
color: var(--kb-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.kb-app-product {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.kb-app-product:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
@@ -85,17 +134,26 @@
|
||||
}
|
||||
|
||||
.kb-app-product__link {
|
||||
padding: 8px 10px;
|
||||
padding: 7px 9px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kb-app-page-list a {
|
||||
padding: 6px 10px 6px 22px;
|
||||
padding: 6px 8px 6px 12px;
|
||||
color: var(--kb-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.kb-app-page-list__part,
|
||||
.kb-page-list__part {
|
||||
display: block;
|
||||
color: var(--kb-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.kb-app-product__link:hover,
|
||||
.kb-app-page-list a:hover,
|
||||
.kb-app-product.is-active > .kb-app-product__link,
|
||||
@@ -106,6 +164,7 @@
|
||||
|
||||
.kb-app-main {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kb-docs-home > h1,
|
||||
@@ -114,10 +173,31 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.kb-docs-home,
|
||||
.kb-docs-product {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kb-docs-home > h1,
|
||||
.kb-docs-product > h1 {
|
||||
margin-bottom: 18px;
|
||||
font-size: 38px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.kb-docs-product {
|
||||
border: 1px solid var(--kb-border);
|
||||
border-radius: var(--kb-radius, 8px);
|
||||
padding: clamp(22px, 2.7vw, 42px);
|
||||
background: var(--kb-surface);
|
||||
box-shadow: var(--kb-shadow, 0 1px 2px rgba(16, 24, 40, 0.04));
|
||||
}
|
||||
|
||||
.kb-product-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.kb-home-card,
|
||||
@@ -132,9 +212,9 @@
|
||||
|
||||
.kb-docs-home-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 0.8fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.kb-home-card h2 {
|
||||
@@ -153,6 +233,23 @@
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kb-product-updates__month {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.kb-product-updates__month:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.kb-docs-wrap .kb-product-updates__month h3 {
|
||||
margin: 0 0 10px;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
background: var(--kb-accent);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.kb-product-updates__list li {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--kb-border);
|
||||
@@ -174,6 +271,15 @@
|
||||
color: var(--kb-text);
|
||||
}
|
||||
|
||||
.kb-product-updates__meta strong a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kb-product-updates__meta strong a:hover {
|
||||
color: var(--kb-accent);
|
||||
}
|
||||
|
||||
.kb-product-updates__meta span,
|
||||
.kb-product-updates__meta time,
|
||||
.kb-empty-state {
|
||||
@@ -181,11 +287,65 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kb-product-card h2 {
|
||||
.kb-product-updates__changes {
|
||||
margin: 8px 0 0 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.kb-product-updates__changes li {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.kb-product-updates__details {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: 10px 0 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kb-product-updates__details div {
|
||||
display: grid;
|
||||
grid-template-columns: 112px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.kb-product-updates__details dt {
|
||||
color: var(--kb-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kb-product-updates__details dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.kb-product-category {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.kb-product-category:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.kb-product-category > h2 {
|
||||
margin: 0 0 12px;
|
||||
color: var(--kb-muted);
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.kb-product-card h2,
|
||||
.kb-product-card h3 {
|
||||
margin-top: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.kb-product-card__parts,
|
||||
.kb-product-parts {
|
||||
color: var(--kb-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kb-product-card ul,
|
||||
.kb-version-list,
|
||||
.kb-page-list {
|
||||
@@ -200,10 +360,28 @@
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
.kb-version-list a,
|
||||
.kb-page-list a {
|
||||
display: block;
|
||||
border: 1px solid var(--kb-border);
|
||||
border-radius: 8px;
|
||||
padding: 11px 12px;
|
||||
background: color-mix(in srgb, var(--kb-surface) 92%, var(--kb-surface-muted));
|
||||
color: var(--kb-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kb-version-list a:hover,
|
||||
.kb-page-list a:hover {
|
||||
border-color: color-mix(in srgb, var(--kb-accent) 38%, var(--kb-border));
|
||||
background: var(--kb-accent-soft);
|
||||
color: var(--kb-accent);
|
||||
}
|
||||
|
||||
.kb-doc-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
gap: 40px;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
gap: clamp(22px, 2.8vw, 42px);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@@ -281,11 +459,12 @@
|
||||
|
||||
.kb-doc-content,
|
||||
.kb-docs-version {
|
||||
max-width: 860px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--kb-border);
|
||||
border-radius: var(--kb-radius, 8px);
|
||||
padding: 22px;
|
||||
padding: clamp(22px, 2.7vw, 42px);
|
||||
background: var(--kb-surface);
|
||||
box-shadow: var(--kb-shadow, 0 1px 2px rgba(16, 24, 40, 0.04));
|
||||
}
|
||||
@@ -305,7 +484,7 @@
|
||||
.kb-doc-header h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: clamp(28px, 4vw, 42px);
|
||||
font-size: 38px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
@@ -377,6 +556,13 @@
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.kb-docs-wrap {
|
||||
left: auto;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 18px 14px 36px;
|
||||
}
|
||||
|
||||
.kb-docs-app {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -407,6 +593,12 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.kb-doc-header h1,
|
||||
.kb-docs-home > h1,
|
||||
.kb-docs-product > h1 {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.kb-version-switcher {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
--kb-border: rgba(52, 53, 55, 0.15);
|
||||
--kb-accent: var(--kb-primary);
|
||||
--kb-accent-soft: rgba(0, 167, 230, 0.1);
|
||||
--kb-radius: 14px;
|
||||
--kb-shadow: 0 18px 40px rgba(52, 53, 55, 0.11);
|
||||
--kb-radius: 8px;
|
||||
--kb-shadow: 0 10px 26px rgba(52, 53, 55, 0.09);
|
||||
--kb-font-body: "Nunito", "Segoe UI", Arial, sans-serif;
|
||||
--kb-font-heading: "Nunito", "Segoe UI", Arial, sans-serif;
|
||||
--kb-font-strong: "URW Gothic L Demi", "Nunito", "Segoe UI", Arial, sans-serif;
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
|
||||
.kb-app-sidebar {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.kb-app-sidebar__brand a {
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
.kb-app-product__link,
|
||||
.kb-app-page-list a {
|
||||
border-radius: 11px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.kb-app-product__link:hover,
|
||||
@@ -100,12 +100,14 @@
|
||||
color: var(--kb-primary-shade);
|
||||
}
|
||||
|
||||
.kb-product-card h2 a {
|
||||
.kb-product-card h2 a,
|
||||
.kb-product-card h3 a {
|
||||
color: var(--kb-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kb-product-card h2 a:hover {
|
||||
.kb-product-card h2 a:hover,
|
||||
.kb-product-card h3 a:hover {
|
||||
color: var(--kb-primary);
|
||||
}
|
||||
|
||||
@@ -139,7 +141,7 @@
|
||||
}
|
||||
|
||||
.kb-rendered-content h2 {
|
||||
font-size: clamp(26px, 3vw, 32px);
|
||||
font-size: 30px;
|
||||
line-height: 1.12;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -19,107 +19,153 @@ final class ProductsPage
|
||||
?>
|
||||
<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>
|
||||
<p><?php esc_html_e('Manage imported products, group them into frontend products, and assign categories.', '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']);
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('kb_markdown_save_all_products'); ?>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<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>
|
||||
<th><?php esc_html_e('Imported product', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Frontend product', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Documentation part', 'kb-markdown-importer'); ?></th>
|
||||
<th><?php esc_html_e('Category', '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('Delete', 'kb-markdown-importer'); ?></th>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (! $products) : ?>
|
||||
<tr><td colspan="7"><?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'];
|
||||
$meta = (array) $item['meta'];
|
||||
$versions = array_map(static fn (\WP_Term $version): string => $version->name, (array) $item['versions']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="products[<?php echo esc_attr((string) $term->term_id); ?>][term_id]" value="<?php echo esc_attr((string) $term->term_id); ?>">
|
||||
<strong><?php echo esc_html($term->name); ?></strong>
|
||||
<br>
|
||||
<code><?php echo esc_html($term->slug); ?></code>
|
||||
<input type="hidden" name="products[<?php echo esc_attr((string) $term->term_id); ?>][product_name]" value="<?php echo esc_attr($term->name); ?>">
|
||||
<input type="hidden" name="products[<?php echo esc_attr((string) $term->term_id); ?>][product_slug]" value="<?php echo esc_attr($term->slug); ?>">
|
||||
</td>
|
||||
<td>
|
||||
<label class="screen-reader-text" for="group_name_<?php echo esc_attr((string) $term->term_id); ?>"><?php esc_html_e('Frontend product name', 'kb-markdown-importer'); ?></label>
|
||||
<input id="group_name_<?php echo esc_attr((string) $term->term_id); ?>" class="regular-text" type="text" name="products[<?php echo esc_attr((string) $term->term_id); ?>][group_name]" value="<?php echo esc_attr((string) $meta['group_name']); ?>" placeholder="<?php echo esc_attr($term->name); ?>">
|
||||
<br>
|
||||
<label class="screen-reader-text" for="group_slug_<?php echo esc_attr((string) $term->term_id); ?>"><?php esc_html_e('Frontend product slug', 'kb-markdown-importer'); ?></label>
|
||||
<input id="group_slug_<?php echo esc_attr((string) $term->term_id); ?>" class="regular-text" type="text" name="products[<?php echo esc_attr((string) $term->term_id); ?>][group_slug]" value="<?php echo esc_attr((string) $meta['group_slug']); ?>" placeholder="<?php echo esc_attr($term->slug); ?>">
|
||||
</td>
|
||||
<td>
|
||||
<input class="regular-text" type="text" name="products[<?php echo esc_attr((string) $term->term_id); ?>][part_label]" value="<?php echo esc_attr((string) $meta['part_label']); ?>" placeholder="<?php esc_attr_e('App, Modul, Exporter', 'kb-markdown-importer'); ?>">
|
||||
</td>
|
||||
<td>
|
||||
<input class="regular-text" type="text" name="products[<?php echo esc_attr((string) $term->term_id); ?>][category]" value="<?php echo esc_attr((string) $meta['category']); ?>" placeholder="<?php esc_attr_e('CRM, Telefonie, Integration', '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>
|
||||
<label>
|
||||
<input type="checkbox" name="delete_terms[]" value="<?php echo esc_attr((string) $term->term_id); ?>">
|
||||
<?php esc_html_e('Trash pages and remove product', 'kb-markdown-importer'); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php submit_button(__('Save all products', 'kb-markdown-importer'), 'primary', 'kb_markdown_save_all_products', false); ?>
|
||||
<?php submit_button(__('Delete selected', 'kb-markdown-importer'), 'delete', 'kb_markdown_delete_selected_products', false, [
|
||||
'onclick' => "return window.confirm('" . esc_js(__('Move selected products and their imported pages to the trash?', 'kb-markdown-importer')) . "');",
|
||||
]); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private static function handleActions(ProductRepository $repository): void
|
||||
{
|
||||
$action = sanitize_key(wp_unslash((string) ($_POST['kb_markdown_product_action'] ?? '')));
|
||||
$action = '';
|
||||
if (isset($_POST['kb_markdown_save_all_products'])) {
|
||||
$action = 'save_all';
|
||||
} elseif (isset($_POST['kb_markdown_delete_selected_products'])) {
|
||||
$action = 'delete_selected';
|
||||
} else {
|
||||
$action = sanitize_key(wp_unslash((string) ($_POST['kb_markdown_product_action'] ?? '')));
|
||||
}
|
||||
|
||||
if (! $action) {
|
||||
return;
|
||||
}
|
||||
|
||||
$termId = absint($_POST['term_id'] ?? 0);
|
||||
if ('save_all' === $action) {
|
||||
check_admin_referer('kb_markdown_save_all_products');
|
||||
$products = (array) ($_POST['products'] ?? []);
|
||||
$saved = 0;
|
||||
|
||||
if (! $termId) {
|
||||
add_settings_error('kb_markdown_products', 'missing_term', __('Missing product ID.', 'kb-markdown-importer'), 'error');
|
||||
foreach ($products as $rawTermId => $rawProduct) {
|
||||
$termId = absint($rawTermId);
|
||||
$rawProduct = (array) $rawProduct;
|
||||
|
||||
if (! $termId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $repository->update(
|
||||
$termId,
|
||||
sanitize_text_field(wp_unslash((string) ($rawProduct['product_name'] ?? ''))),
|
||||
sanitize_title(wp_unslash((string) ($rawProduct['product_slug'] ?? ''))),
|
||||
[
|
||||
'group_name' => sanitize_text_field(wp_unslash((string) ($rawProduct['group_name'] ?? ''))),
|
||||
'group_slug' => sanitize_title(wp_unslash((string) ($rawProduct['group_slug'] ?? ''))),
|
||||
'part_label' => sanitize_text_field(wp_unslash((string) ($rawProduct['part_label'] ?? ''))),
|
||||
'category' => sanitize_text_field(wp_unslash((string) ($rawProduct['category'] ?? ''))),
|
||||
]
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
add_settings_error('kb_markdown_products', 'update_failed_' . $termId, $result->get_error_message(), 'error');
|
||||
continue;
|
||||
}
|
||||
|
||||
$saved++;
|
||||
}
|
||||
|
||||
add_settings_error('kb_markdown_products', 'updated', sprintf(
|
||||
/* translators: %d: number of saved products. */
|
||||
__('Saved %d products.', 'kb-markdown-importer'),
|
||||
$saved
|
||||
), 'success');
|
||||
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 ('delete_selected' === $action) {
|
||||
check_admin_referer('kb_markdown_save_all_products');
|
||||
$termIds = array_map('absint', (array) ($_POST['delete_terms'] ?? []));
|
||||
$deleted = 0;
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
add_settings_error('kb_markdown_products', 'update_failed', $result->get_error_message(), 'error');
|
||||
return;
|
||||
foreach (array_filter($termIds) as $termId) {
|
||||
$result = $repository->deleteProduct($termId, true);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
add_settings_error('kb_markdown_products', 'delete_failed_' . $termId, $result->get_error_message(), 'error');
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleted++;
|
||||
}
|
||||
|
||||
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');
|
||||
add_settings_error('kb_markdown_products', 'deleted', sprintf(
|
||||
/* translators: %d: number of deleted products. */
|
||||
__('Deleted %d products.', 'kb-markdown-importer'),
|
||||
$deleted
|
||||
), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace KbMarkdownImporter\Admin;
|
||||
|
||||
use KbMarkdownImporter\GitLab\GitLabClient;
|
||||
use KbMarkdownImporter\Import\ImportLogger;
|
||||
use KbMarkdownImporter\Olm\ChangelogSync;
|
||||
use KbMarkdownImporter\Plugin;
|
||||
use KbMarkdownImporter\Settings;
|
||||
|
||||
@@ -40,20 +41,12 @@ final class SettingsPage
|
||||
$settings['custom_theme_css_url'] = esc_url_raw((string) ($input['custom_theme_css_url'] ?? ''));
|
||||
$settings['docs_home_intro_title'] = sanitize_text_field((string) ($input['docs_home_intro_title'] ?? $settings['docs_home_intro_title']));
|
||||
$settings['docs_home_intro_content'] = wp_kses_post((string) ($input['docs_home_intro_content'] ?? $settings['docs_home_intro_content']));
|
||||
$settings['product_updates_source'] = in_array(($input['product_updates_source'] ?? 'rss'), ['rss', 'rest'], true) ? (string) $input['product_updates_source'] : 'rss';
|
||||
$settings['product_updates_feed_url'] = esc_url_raw((string) ($input['product_updates_feed_url'] ?? ''));
|
||||
$settings['product_updates_feed_limit'] = (string) max(1, min(20, (int) ($input['product_updates_feed_limit'] ?? 5)));
|
||||
$settings['product_updates_feed_item_path'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_item_path'] ?? 'channel/item'), 'channel/item');
|
||||
$settings['product_updates_feed_product_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_product_field'] ?? 'title'), 'title');
|
||||
$settings['product_updates_feed_version_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_version_field'] ?? 'category'), 'category');
|
||||
$settings['product_updates_feed_date_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_date_field'] ?? 'pubDate'), 'pubDate');
|
||||
$settings['product_updates_feed_changelog_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_changelog_field'] ?? 'description'), 'description');
|
||||
$settings['product_updates_rest_url'] = esc_url_raw((string) ($input['product_updates_rest_url'] ?? ''));
|
||||
$settings['product_updates_rest_list_path'] = self::sanitizePathList((string) ($input['product_updates_rest_list_path'] ?? 'content,data,items'), 'content,data,items');
|
||||
$settings['product_updates_rest_product_field'] = self::sanitizePathList((string) ($input['product_updates_rest_product_field'] ?? 'product.name,productName,name'), 'product.name,productName,name');
|
||||
$settings['product_updates_rest_version_field'] = self::sanitizePathList((string) ($input['product_updates_rest_version_field'] ?? 'version,versionName,name'), 'version,versionName,name');
|
||||
$settings['product_updates_rest_date_field'] = self::sanitizePathList((string) ($input['product_updates_rest_date_field'] ?? 'releaseDate,date,updatedAt,createdAt'), 'releaseDate,date,updatedAt,createdAt');
|
||||
$settings['product_updates_rest_changelog_field'] = self::sanitizePathList((string) ($input['product_updates_rest_changelog_field'] ?? 'changelog,changeLog,description,changes'), 'changelog,changeLog,description,changes');
|
||||
$settings['product_updates_source'] = 'olm_changelog';
|
||||
$settings['product_updates_olm_months'] = (string) max(1, min(24, (int) ($input['product_updates_olm_months'] ?? 4)));
|
||||
$settings['product_updates_olm_ignore_numbers'] = self::sanitizeOlmNumberList((string) ($input['product_updates_olm_ignore_numbers'] ?? 'olm-10109,olm-10110'));
|
||||
$settings['olm_base_url'] = esc_url_raw(ChangelogSync::normalizeBaseUrl((string) ($input['olm_base_url'] ?? '')));
|
||||
$settings['olm_username'] = sanitize_text_field((string) ($input['olm_username'] ?? ''));
|
||||
$settings['olm_password'] = trim((string) ($input['olm_password'] ?? '')) ?: (string) $old['olm_password'];
|
||||
|
||||
Plugin::syncCronSchedule($settings);
|
||||
if (($old['docs_base_slug'] ?? 'docs') !== $settings['docs_base_slug']) {
|
||||
@@ -155,93 +148,33 @@ final class SettingsPage
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_source"><?php esc_html_e('Update-Quelle', 'kb-markdown-importer'); ?></label></th>
|
||||
<th scope="row"><label for="product_updates_olm_months"><?php esc_html_e('OLM Zeitraum', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<select id="product_updates_source" name="kb_markdown_importer_settings[product_updates_source]">
|
||||
<option value="rss" <?php selected($settings['product_updates_source'], 'rss'); ?>><?php esc_html_e('RSS/XML', 'kb-markdown-importer'); ?></option>
|
||||
<option value="rest" <?php selected($settings['product_updates_source'], 'rest'); ?>><?php esc_html_e('REST/JSON', 'kb-markdown-importer'); ?></option>
|
||||
</select>
|
||||
<input id="product_updates_olm_months" name="kb_markdown_importer_settings[product_updates_olm_months]" type="number" min="1" max="24" value="<?php echo esc_attr($settings['product_updates_olm_months']); ?>"> <?php esc_html_e('Monate zurück ab Monatsanfang', 'kb-markdown-importer'); ?>
|
||||
<p class="description"><?php esc_html_e('Entspricht dem Python-Script: aktueller Monat plus die angegebene Anzahl vorheriger Monate.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_feed_url"><?php esc_html_e('RSS/XML-Feed URL', 'kb-markdown-importer'); ?></label></th>
|
||||
<th scope="row"><label for="product_updates_olm_ignore_numbers"><?php esc_html_e('OLM Nummern ignorieren', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_feed_url" name="kb_markdown_importer_settings[product_updates_feed_url]" type="url" value="<?php echo esc_attr($settings['product_updates_feed_url']); ?>" placeholder="https://example.com/updates.xml">
|
||||
<p class="description"><?php esc_html_e('RSS- oder XML-Feed mit den neuesten Produktupdates. Wird nur genutzt, wenn RSS/XML als Quelle ausgewählt ist.', 'kb-markdown-importer'); ?></p>
|
||||
<input class="regular-text" id="product_updates_olm_ignore_numbers" name="kb_markdown_importer_settings[product_updates_olm_ignore_numbers]" type="text" value="<?php echo esc_attr($settings['product_updates_olm_ignore_numbers']); ?>" placeholder="olm-10109,olm-10110">
|
||||
<p class="description"><?php esc_html_e('Kommagetrennte productNo-Liste, die nicht im Changelog erscheinen soll.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_rest_url"><?php esc_html_e('REST/JSON URL', 'kb-markdown-importer'); ?></label></th>
|
||||
<th scope="row"><label for="olm_base_url"><?php esc_html_e('OLM Basis-URL', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_rest_url" name="kb_markdown_importer_settings[product_updates_rest_url]" type="url" value="<?php echo esc_attr($settings['product_updates_rest_url']); ?>" placeholder="https://example.com/api/product-versions">
|
||||
<p class="description"><?php esc_html_e('REST-Endpunkt mit JSON-Antwort. Wird nur genutzt, wenn REST/JSON als Quelle ausgewählt ist.', 'kb-markdown-importer'); ?></p>
|
||||
<input class="regular-text" id="olm_base_url" name="kb_markdown_importer_settings[olm_base_url]" type="url" value="<?php echo esc_attr($settings['olm_base_url']); ?>" placeholder="https://olm.o-byte.com">
|
||||
<p class="description"><?php esc_html_e('Wird für den OLM-Changelog-Sync nach dem Python-Script verwendet.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_feed_limit"><?php esc_html_e('Anzahl Updates', 'kb-markdown-importer'); ?></label></th>
|
||||
<td><input id="product_updates_feed_limit" name="kb_markdown_importer_settings[product_updates_feed_limit]" type="number" min="1" max="20" value="<?php echo esc_attr($settings['product_updates_feed_limit']); ?>"></td>
|
||||
<th scope="row"><label for="olm_username"><?php esc_html_e('OLM Benutzername', 'kb-markdown-importer'); ?></label></th>
|
||||
<td><input class="regular-text" id="olm_username" name="kb_markdown_importer_settings[olm_username]" type="text" value="<?php echo esc_attr($settings['olm_username']); ?>"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_feed_item_path"><?php esc_html_e('Eintrag-Pfad', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_feed_item_path" name="kb_markdown_importer_settings[product_updates_feed_item_path]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_item_path']); ?>" placeholder="channel/item">
|
||||
<p class="description"><?php esc_html_e('Pfad zum wiederholten Feed-Eintrag, zum Beispiel channel/item.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_rest_list_path"><?php esc_html_e('REST Listenpfad', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_rest_list_path" name="kb_markdown_importer_settings[product_updates_rest_list_path]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_list_path']); ?>" placeholder="content,data,items">
|
||||
<p class="description"><?php esc_html_e('Pfad zur Liste in der JSON-Antwort. Mehrere Alternativen mit Komma trennen. Leer lassen, wenn die Antwort direkt ein Array ist.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('XML-Felder', 'kb-markdown-importer'); ?></th>
|
||||
<td>
|
||||
<fieldset class="kb-feed-fields">
|
||||
<p>
|
||||
<label for="product_updates_feed_product_field"><?php esc_html_e('Produktname', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_product_field" name="kb_markdown_importer_settings[product_updates_feed_product_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_product_field']); ?>" placeholder="title">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_feed_version_field"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_version_field" name="kb_markdown_importer_settings[product_updates_feed_version_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_version_field']); ?>" placeholder="category">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_feed_date_field"><?php esc_html_e('Datum', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_date_field" name="kb_markdown_importer_settings[product_updates_feed_date_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_date_field']); ?>" placeholder="pubDate">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_feed_changelog_field"><?php esc_html_e('Changelog', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_changelog_field" name="kb_markdown_importer_settings[product_updates_feed_changelog_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_changelog_field']); ?>" placeholder="description">
|
||||
</p>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('Feldpfade relativ zum Eintrag, zum Beispiel product/name, version oder changelog. Namespaces wie dc:date werden unterstützt.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('REST-Felder', 'kb-markdown-importer'); ?></th>
|
||||
<td>
|
||||
<fieldset class="kb-rest-fields">
|
||||
<p>
|
||||
<label for="product_updates_rest_product_field"><?php esc_html_e('Produktname', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_product_field" name="kb_markdown_importer_settings[product_updates_rest_product_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_product_field']); ?>" placeholder="product.name,productName,name">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_rest_version_field"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_version_field" name="kb_markdown_importer_settings[product_updates_rest_version_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_version_field']); ?>" placeholder="version,versionName,name">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_rest_date_field"><?php esc_html_e('Datum', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_date_field" name="kb_markdown_importer_settings[product_updates_rest_date_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_date_field']); ?>" placeholder="releaseDate,date,updatedAt,createdAt">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_rest_changelog_field"><?php esc_html_e('Changelog', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_changelog_field" name="kb_markdown_importer_settings[product_updates_rest_changelog_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_changelog_field']); ?>" placeholder="changelog,changeLog,description,changes">
|
||||
</p>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('JSON-Feldpfade relativ zu einem Eintrag. Verschachtelte Felder mit Punkt oder Slash angeben, Alternativen mit Komma trennen.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
<th scope="row"><label for="olm_password"><?php esc_html_e('OLM Passwort', 'kb-markdown-importer'); ?></label></th>
|
||||
<td><input class="regular-text" id="olm_password" name="kb_markdown_importer_settings[olm_password]" type="password" value="" placeholder="<?php echo $settings['olm_password'] ? esc_attr__('Passwort ist gespeichert; leer lassen zum Beibehalten', 'kb-markdown-importer') : ''; ?>"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2><?php esc_html_e('Frontend Design', 'kb-markdown-importer'); ?></h2>
|
||||
@@ -287,8 +220,8 @@ final class SettingsPage
|
||||
</form>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('kb_markdown_test_product_updates'); ?>
|
||||
<?php submit_button(__('Produktupdate-Quelle testen', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_product_updates'); ?>
|
||||
<p class="description"><?php esc_html_e('Der Test nutzt die gespeicherten Einstellungen der ausgewählten Update-Quelle. Bitte Änderungen vorher speichern.', 'kb-markdown-importer'); ?></p>
|
||||
<?php submit_button(__('OLM Changelog synchronisieren', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_product_updates'); ?>
|
||||
<p class="description"><?php esc_html_e('Nutzt die gespeicherten OLM-Einstellungen. Bitte Änderungen vorher speichern.', 'kb-markdown-importer'); ?></p>
|
||||
</form>
|
||||
<?php if (is_array($updatesTest)) : ?>
|
||||
<div class="notice notice-<?php echo $updatesTest['ok'] ? 'success' : 'error'; ?>">
|
||||
@@ -296,7 +229,7 @@ final class SettingsPage
|
||||
<p><?php echo esc_html($updatesTest['message']); ?></p>
|
||||
</div>
|
||||
<?php if ('' !== $updatesTest['body']) : ?>
|
||||
<h2><?php esc_html_e('Antwort der Produktupdate-Quelle', 'kb-markdown-importer'); ?></h2>
|
||||
<h2><?php esc_html_e('Gespeicherte Changelog-Vorschau', 'kb-markdown-importer'); ?></h2>
|
||||
<textarea class="large-text code" rows="16" readonly><?php echo esc_textarea($updatesTest['body']); ?></textarea>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
@@ -324,73 +257,23 @@ final class SettingsPage
|
||||
|
||||
private static function handleProductUpdatesTest(): array
|
||||
{
|
||||
$settings = Plugin::settings();
|
||||
$source = (string) ($settings['product_updates_source'] ?? 'rss');
|
||||
$url = esc_url_raw((string) ('rest' === $source ? ($settings['product_updates_rest_url'] ?? '') : ($settings['product_updates_feed_url'] ?? '')));
|
||||
|
||||
if ('' === $url) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'title' => __('Keine Produktupdate-Quelle konfiguriert.', 'kb-markdown-importer'),
|
||||
'message' => __('Bitte zuerst eine RSS/XML- oder REST/JSON-URL speichern.', 'kb-markdown-importer'),
|
||||
'body' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => 12,
|
||||
'redirection' => 3,
|
||||
'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'title' => __('Produktupdate-Quelle nicht erreichbar.', 'kb-markdown-importer'),
|
||||
'message' => $response->get_error_message(),
|
||||
'body' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$status = (int) wp_remote_retrieve_response_code($response);
|
||||
$contentType = (string) wp_remote_retrieve_header($response, 'content-type');
|
||||
$body = (string) wp_remote_retrieve_body($response);
|
||||
$excerpt = substr($body, 0, 12000);
|
||||
$validPayload = true;
|
||||
$payloadNote = '';
|
||||
|
||||
if ('rest' === $source) {
|
||||
json_decode($body, true);
|
||||
$validPayload = JSON_ERROR_NONE === json_last_error();
|
||||
if (! $validPayload) {
|
||||
$payloadNote = ' ' . sprintf(
|
||||
/* translators: %s: JSON parser error message. */
|
||||
__('Die Antwort ist kein gültiges JSON: %s', 'kb-markdown-importer'),
|
||||
json_last_error_msg()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: 1: source type, 2: HTTP status code, 3: content type. */
|
||||
__('Quelle: %1$s | HTTP-Status: %2$d | Content-Type: %3$s', 'kb-markdown-importer'),
|
||||
'rest' === $source ? 'REST/JSON' : 'RSS/XML',
|
||||
$status,
|
||||
$contentType ?: '-'
|
||||
);
|
||||
$message .= $payloadNote;
|
||||
|
||||
if (strlen($body) > strlen($excerpt)) {
|
||||
$message .= ' ' . __('Die Antwort wurde auf 12000 Zeichen gekürzt.', 'kb-markdown-importer');
|
||||
}
|
||||
|
||||
$ok = $status >= 200 && $status < 300 && $validPayload;
|
||||
$response = (new ChangelogSync())->sync();
|
||||
$data = (array) $response->get_data();
|
||||
$ok = true === ($data['success'] ?? false);
|
||||
$items = ChangelogSync::items();
|
||||
$preview = wp_json_encode(array_slice($items, 0, 5), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return [
|
||||
'ok' => $ok,
|
||||
'title' => $ok ? __('Produktupdate-Quelle erreichbar.', 'kb-markdown-importer') : __('Produktupdate-Quelle nicht nutzbar.', 'kb-markdown-importer'),
|
||||
'message' => $message,
|
||||
'body' => $excerpt,
|
||||
'title' => $ok ? __('OLM Changelog synchronisiert.', 'kb-markdown-importer') : __('OLM Changelog konnte nicht synchronisiert werden.', 'kb-markdown-importer'),
|
||||
'message' => $ok
|
||||
? sprintf(
|
||||
/* translators: %d: number of parsed changelog items. */
|
||||
__('Gefundene Changelog-Einträge im Zeitraum: %d', 'kb-markdown-importer'),
|
||||
count($items)
|
||||
)
|
||||
: (string) ($data['message'] ?? __('Unbekannter Fehler.', 'kb-markdown-importer')),
|
||||
'body' => $preview ?: '',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -425,19 +308,12 @@ final class SettingsPage
|
||||
return preg_match('/^#[0-9a-fA-F]{6}$/', $value) ? strtoupper($value) : $fallback;
|
||||
}
|
||||
|
||||
private static function sanitizeXmlPath(string $value, string $fallback): string
|
||||
private static function sanitizeOlmNumberList(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$value = preg_replace('/[^A-Za-z0-9_:@.\/-]/', '', $value) ?: '';
|
||||
$items = array_filter(array_map(static function (string $item): string {
|
||||
return strtolower(preg_replace('/[^a-zA-Z0-9_-]/', '', trim($item)) ?: '');
|
||||
}, explode(',', $value)), static fn (string $item): bool => '' !== $item);
|
||||
|
||||
return '' !== $value ? $value : $fallback;
|
||||
}
|
||||
|
||||
private static function sanitizePathList(string $value, string $fallback): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$value = preg_replace('/[^A-Za-z0-9_:@.\/,-]/', '', $value) ?: '';
|
||||
|
||||
return '' !== $value ? $value : $fallback;
|
||||
return implode(',', array_values(array_unique($items)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace KbMarkdownImporter\Admin;
|
||||
|
||||
use KbMarkdownImporter\Import\ImportLogger;
|
||||
use KbMarkdownImporter\Olm\ChangelogSync;
|
||||
use KbMarkdownImporter\Plugin;
|
||||
|
||||
final class StatusPage
|
||||
@@ -26,6 +27,8 @@ final class StatusPage
|
||||
<div class="kb-admin-card"><strong>Versions</strong><span><?php echo esc_html((string) $counts['versions']); ?></span></div>
|
||||
<div class="kb-admin-card"><strong>Pages</strong><span><?php echo esc_html((string) $counts['pages']); ?></span></div>
|
||||
<div class="kb-admin-card"><strong>Last sync</strong><span><?php echo esc_html((string) get_option('kb_markdown_importer_last_sync', __('Never', 'kb-markdown-importer'))); ?></span></div>
|
||||
<div class="kb-admin-card"><strong>OLM updates</strong><span><?php echo esc_html((string) count(ChangelogSync::items())); ?></span></div>
|
||||
<div class="kb-admin-card"><strong>Last OLM sync</strong><span><?php echo esc_html(ChangelogSync::lastSync() ?: __('Never', 'kb-markdown-importer')); ?></span></div>
|
||||
<div class="kb-admin-card"><strong>Format</strong><span>Markdown</span></div>
|
||||
</div>
|
||||
<h2><?php esc_html_e('Recent Import Logs', 'kb-markdown-importer'); ?></h2>
|
||||
@@ -40,6 +43,7 @@ final class StatusPage
|
||||
'settings_complete' => (bool) (Plugin::settings()['gitlab_base_url'] && Plugin::settings()['gitlab_token']),
|
||||
'counts' => self::counts(),
|
||||
'last_sync' => get_option('kb_markdown_importer_last_sync', ''),
|
||||
'last_changelog_sync' => ChangelogSync::lastSync(),
|
||||
'last_error' => get_option('kb_markdown_importer_last_error', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace KbMarkdownImporter\Admin;
|
||||
use KbMarkdownImporter\GitLab\GitLabClient;
|
||||
use KbMarkdownImporter\Import\ImportLogger;
|
||||
use KbMarkdownImporter\Import\ImportManager;
|
||||
use KbMarkdownImporter\Olm\ChangelogSync;
|
||||
use KbMarkdownImporter\Plugin;
|
||||
|
||||
final class SyncPage
|
||||
@@ -24,6 +25,7 @@ final class SyncPage
|
||||
<form method="post" class="kb-sync-actions">
|
||||
<?php wp_nonce_field('kb_markdown_sync'); ?>
|
||||
<?php submit_button(__('Sync All', 'kb-markdown-importer'), 'primary', 'kb_markdown_sync_all', false); ?>
|
||||
<?php submit_button(__('Sync OLM Changelog', 'kb-markdown-importer'), 'secondary', 'kb_markdown_sync_changelog', false); ?>
|
||||
<?php submit_button(__('Dry Run', 'kb-markdown-importer'), 'secondary', 'kb_markdown_dry_run', false); ?>
|
||||
</form>
|
||||
|
||||
@@ -63,9 +65,20 @@ final class SyncPage
|
||||
{
|
||||
if (isset($_POST['kb_markdown_sync_all']) && check_admin_referer('kb_markdown_sync')) {
|
||||
(new ImportManager())->syncAll(false);
|
||||
(new ChangelogSync())->sync();
|
||||
echo '<div class="notice notice-success"><p>' . esc_html__('Synchronization finished.', 'kb-markdown-importer') . '</p></div>';
|
||||
}
|
||||
|
||||
if (isset($_POST['kb_markdown_sync_changelog']) && check_admin_referer('kb_markdown_sync')) {
|
||||
$response = (new ChangelogSync())->sync();
|
||||
$data = (array) $response->get_data();
|
||||
$success = true === ($data['success'] ?? false);
|
||||
$message = $success
|
||||
? __('OLM changelog synchronization finished.', 'kb-markdown-importer')
|
||||
: (string) ($data['message'] ?? __('OLM changelog synchronization failed.', 'kb-markdown-importer'));
|
||||
echo '<div class="notice notice-' . ($success ? 'success' : 'error') . '"><p>' . esc_html($message) . '</p></div>';
|
||||
}
|
||||
|
||||
if (isset($_POST['kb_markdown_dry_run']) && check_admin_referer('kb_markdown_sync')) {
|
||||
(new ImportManager())->syncAll(true);
|
||||
echo '<div class="notice notice-info"><p>' . esc_html__('Dry run finished.', 'kb-markdown-importer') . '</p></div>';
|
||||
|
||||
@@ -3,264 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace KbMarkdownImporter\Frontend;
|
||||
|
||||
use KbMarkdownImporter\Plugin;
|
||||
use KbMarkdownImporter\Olm\ChangelogSync;
|
||||
|
||||
final class ProductUpdatesFeed
|
||||
{
|
||||
public static function items(array $settings = []): array
|
||||
{
|
||||
$settings = $settings ?: Plugin::settings();
|
||||
$source = (string) ($settings['product_updates_source'] ?? 'rss');
|
||||
$url = esc_url_raw((string) ('rest' === $source ? ($settings['product_updates_rest_url'] ?? '') : ($settings['product_updates_feed_url'] ?? '')));
|
||||
|
||||
if ('' === $url) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cacheKey = 'kb_product_updates_' . md5($source . $url . wp_json_encode([
|
||||
$settings['product_updates_feed_item_path'] ?? '',
|
||||
$settings['product_updates_feed_product_field'] ?? '',
|
||||
$settings['product_updates_feed_version_field'] ?? '',
|
||||
$settings['product_updates_feed_date_field'] ?? '',
|
||||
$settings['product_updates_feed_changelog_field'] ?? '',
|
||||
$settings['product_updates_rest_list_path'] ?? '',
|
||||
$settings['product_updates_rest_product_field'] ?? '',
|
||||
$settings['product_updates_rest_version_field'] ?? '',
|
||||
$settings['product_updates_rest_date_field'] ?? '',
|
||||
$settings['product_updates_rest_changelog_field'] ?? '',
|
||||
]));
|
||||
$cached = get_transient($cacheKey);
|
||||
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => 8,
|
||||
'redirection' => 3,
|
||||
'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = (string) wp_remote_retrieve_body($response);
|
||||
$items = 'rest' === $source ? self::parseJson($body, $settings) : self::parseXml($body, $settings);
|
||||
set_transient($cacheKey, $items, 15 * MINUTE_IN_SECONDS);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private static function parseXml(string $xml, array $settings): array
|
||||
{
|
||||
if ('' === trim($xml) || ! class_exists(\DOMDocument::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
$document = new \DOMDocument();
|
||||
$loaded = $document->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
|
||||
if (! $loaded) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
$itemNodes = $xpath->query(self::itemPath((string) ($settings['product_updates_feed_item_path'] ?? 'channel/item')));
|
||||
|
||||
if (! $itemNodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$limit = max(1, min(20, (int) ($settings['product_updates_feed_limit'] ?? 5)));
|
||||
$items = [];
|
||||
|
||||
foreach ($itemNodes as $itemNode) {
|
||||
if (count($items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
$date = self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_date_field'] ?? 'pubDate'));
|
||||
$items[] = [
|
||||
'product' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_product_field'] ?? 'title')),
|
||||
'version' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_version_field'] ?? 'category')),
|
||||
'date' => self::formatDate($date),
|
||||
'changelog' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_changelog_field'] ?? 'description')),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private static function parseJson(string $json, array $settings): array
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (! is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$nodes = self::jsonList($data, (string) ($settings['product_updates_rest_list_path'] ?? 'content,data,items'));
|
||||
|
||||
if (! $nodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$limit = max(1, min(20, (int) ($settings['product_updates_feed_limit'] ?? 5)));
|
||||
$items = [];
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
if (count($items) >= $limit || ! is_array($node)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$date = self::jsonField($node, (string) ($settings['product_updates_rest_date_field'] ?? 'releaseDate,date,updatedAt,createdAt'));
|
||||
$items[] = [
|
||||
'product' => self::jsonField($node, (string) ($settings['product_updates_rest_product_field'] ?? 'product.name,productName,name')),
|
||||
'version' => self::jsonField($node, (string) ($settings['product_updates_rest_version_field'] ?? 'version,versionName,name')),
|
||||
'date' => self::formatDate($date),
|
||||
'changelog' => self::jsonField($node, (string) ($settings['product_updates_rest_changelog_field'] ?? 'changelog,changeLog,description,changes')),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private static function jsonList(array $data, string $paths): array
|
||||
{
|
||||
foreach (self::pathAlternatives($paths) as $path) {
|
||||
$value = '' === $path ? $data : self::jsonValue($data, $path);
|
||||
|
||||
if (is_array($value) && array_is_list($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return array_is_list($data) ? $data : [];
|
||||
}
|
||||
|
||||
private static function jsonField(array $data, string $paths): string
|
||||
{
|
||||
foreach (self::pathAlternatives($paths) as $path) {
|
||||
$value = self::jsonValue($data, $path);
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return trim(wp_strip_all_tags((string) $value));
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$text = implode(', ', array_filter(array_map(static fn ($item): string => is_scalar($item) ? (string) $item : '', $value)));
|
||||
if ('' !== $text) {
|
||||
return trim(wp_strip_all_tags($text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function jsonValue(array $data, string $path): mixed
|
||||
{
|
||||
$value = $data;
|
||||
|
||||
foreach (self::pathSegments($path) as $segment) {
|
||||
if (! is_array($value) || ! array_key_exists($segment, $value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $value[$segment];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function pathAlternatives(string $paths): array
|
||||
{
|
||||
return array_values(array_map('trim', explode(',', $paths)));
|
||||
}
|
||||
|
||||
private static function itemPath(string $path): string
|
||||
{
|
||||
$segments = self::pathSegments($path ?: 'channel/item');
|
||||
$first = (string) array_shift($segments);
|
||||
$query = '//*[local-name()="' . self::localName($first ?: 'item') . '"]';
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
$query .= '/*[local-name()="' . self::localName((string) $segment) . '"]';
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private static function fieldValue(\DOMXPath $xpath, \DOMNode $node, string $path): string
|
||||
{
|
||||
$segments = self::pathSegments($path);
|
||||
|
||||
if (! $segments) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attribute = null;
|
||||
$last = (string) end($segments);
|
||||
|
||||
if (str_starts_with($last, '@')) {
|
||||
$attribute = substr($last, 1);
|
||||
array_pop($segments);
|
||||
}
|
||||
|
||||
$query = '.';
|
||||
foreach ($segments as $segment) {
|
||||
$query .= '/*[local-name()="' . self::localName((string) $segment) . '"]';
|
||||
}
|
||||
|
||||
$result = $xpath->query($query, $node);
|
||||
$target = $result && $result->length > 0 ? $result->item(0) : null;
|
||||
|
||||
if (! $target) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($attribute && $target instanceof \DOMElement) {
|
||||
return trim(wp_strip_all_tags(html_entity_decode($target->getAttribute($attribute), ENT_QUOTES | ENT_XML1, get_bloginfo('charset'))));
|
||||
}
|
||||
|
||||
return trim(wp_strip_all_tags(html_entity_decode($target->textContent, ENT_QUOTES | ENT_XML1, get_bloginfo('charset'))));
|
||||
}
|
||||
|
||||
private static function pathSegments(string $path): array
|
||||
{
|
||||
$path = trim(str_replace('.', '/', $path), '/ ');
|
||||
|
||||
if ('' === $path) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map('trim', explode('/', $path)), static fn (string $segment): bool => '' !== $segment));
|
||||
}
|
||||
|
||||
private static function localName(string $segment): string
|
||||
{
|
||||
$segment = trim($segment);
|
||||
$parts = explode(':', $segment);
|
||||
|
||||
return preg_replace('/[^A-Za-z0-9_-]/', '', (string) end($parts)) ?: '';
|
||||
}
|
||||
|
||||
private static function formatDate(string $date): string
|
||||
{
|
||||
if ('' === $date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$timestamp = strtotime($date);
|
||||
|
||||
if (! $timestamp) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
return wp_date((string) get_option('date_format'), $timestamp);
|
||||
return ChangelogSync::items();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace KbMarkdownImporter\Frontend;
|
||||
use KbMarkdownImporter\Access\AccessController;
|
||||
use KbMarkdownImporter\Plugin;
|
||||
use KbMarkdownImporter\Repository\PageRepository;
|
||||
use KbMarkdownImporter\Repository\ProductRepository;
|
||||
|
||||
final class Router
|
||||
{
|
||||
@@ -212,15 +213,18 @@ final class Router
|
||||
|
||||
private function captureProduct(string $productSlug): string
|
||||
{
|
||||
$product = get_term_by('slug', $productSlug, 'kb_product');
|
||||
$productItem = self::frontendProduct($productSlug);
|
||||
$product = $productItem['term'] ?? null;
|
||||
if (! $product) {
|
||||
return $this->capture404();
|
||||
}
|
||||
$productSlug = (string) $product->slug;
|
||||
|
||||
$versions = $this->versionsForProduct($productSlug);
|
||||
|
||||
return (new TemplateLoader())->capture('product', [
|
||||
'product' => $product,
|
||||
'product_item' => $productItem,
|
||||
'versions' => $versions,
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
@@ -234,23 +238,30 @@ final class Router
|
||||
|
||||
private function captureVersion(string $productSlug, string $versionSlug): string
|
||||
{
|
||||
$product = get_term_by('slug', $productSlug, 'kb_product');
|
||||
$productItem = self::frontendProduct($productSlug);
|
||||
$product = $productItem['term'] ?? null;
|
||||
$version = get_term_by('slug', $versionSlug, 'kb_version');
|
||||
|
||||
if (! $product || ! $version) {
|
||||
return $this->capture404();
|
||||
}
|
||||
$productSlug = (string) $product->slug;
|
||||
|
||||
$landing = $this->landingPageForVersion($productSlug, $versionSlug);
|
||||
if ($landing) {
|
||||
return $this->captureDocPage($landing, $productSlug, $versionSlug);
|
||||
$landingSlugs = $this->pageLinkSlugs([$landing], $productSlug);
|
||||
return $this->captureDocPage($landing, $productSlug, $versionSlug, $productItem, (string) ($landingSlugs[$landing->ID] ?? ''));
|
||||
}
|
||||
|
||||
$pages = $this->pagesForVersion($productSlug, $versionSlug);
|
||||
|
||||
return (new TemplateLoader())->capture('version', [
|
||||
'product' => $product,
|
||||
'product_item' => $productItem,
|
||||
'version' => $version,
|
||||
'versions' => $this->versionsForProduct($productSlug),
|
||||
'pages' => $this->pagesForVersion($productSlug, $versionSlug),
|
||||
'pages' => $pages,
|
||||
'page_link_slugs' => $this->pageLinkSlugs($pages, $productSlug),
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
]);
|
||||
@@ -263,17 +274,35 @@ final class Router
|
||||
|
||||
private function capturePage(string $productSlug, string $versionSlug, string $pageSlug): string
|
||||
{
|
||||
$post = (new PageRepository())->findFrontendPage($productSlug, $versionSlug, $pageSlug);
|
||||
$productItem = self::frontendProduct($productSlug);
|
||||
if ($productItem && isset($productItem['term']->slug)) {
|
||||
$productSlug = (string) $productItem['term']->slug;
|
||||
}
|
||||
$termSlugs = $this->sourceProductSlugs($productSlug);
|
||||
$sourceSlug = '';
|
||||
$realPageSlug = $pageSlug;
|
||||
|
||||
if (! $post && '' === $pageSlug) {
|
||||
$post = (new PageRepository())->findFrontendPage($productSlug, $versionSlug, 'index');
|
||||
if (str_contains($pageSlug, '--')) {
|
||||
[$sourceSlug, $realPageSlug] = array_pad(explode('--', $pageSlug, 2), 2, '');
|
||||
if (in_array($sourceSlug, $termSlugs, true)) {
|
||||
$termSlugs = [$sourceSlug];
|
||||
}
|
||||
if ('index' === $realPageSlug) {
|
||||
$realPageSlug = '';
|
||||
}
|
||||
}
|
||||
|
||||
$post = (new PageRepository())->findFrontendPageInProducts($termSlugs, $versionSlug, $realPageSlug);
|
||||
|
||||
if (! $post && '' === $realPageSlug) {
|
||||
$post = (new PageRepository())->findFrontendPageInProducts($termSlugs, $versionSlug, 'index');
|
||||
}
|
||||
|
||||
if (! $post) {
|
||||
return $this->capture404();
|
||||
}
|
||||
|
||||
return $this->captureDocPage($post, $productSlug, $versionSlug);
|
||||
return $this->captureDocPage($post, $productSlug, $versionSlug, $productItem, $pageSlug);
|
||||
}
|
||||
|
||||
private function renderDocPage(\WP_Post $post, string $productSlug, string $versionSlug): void
|
||||
@@ -281,21 +310,26 @@ final class Router
|
||||
echo $this->captureDocPage($post, $productSlug, $versionSlug);
|
||||
}
|
||||
|
||||
private function captureDocPage(\WP_Post $post, string $productSlug, string $versionSlug): string
|
||||
private function captureDocPage(\WP_Post $post, string $productSlug, string $versionSlug, ?array $productItem = null, string $activePageSlug = ''): string
|
||||
{
|
||||
$product = get_term_by('slug', $productSlug, 'kb_product');
|
||||
$productItem = $productItem ?: self::frontendProduct($productSlug);
|
||||
$product = $productItem['term'] ?? null;
|
||||
$version = get_term_by('slug', $versionSlug, 'kb_version');
|
||||
$navTree = json_decode((string) get_post_meta($post->ID, '_kb_nav_tree', true), true);
|
||||
$pages = $this->pagesForVersion($productSlug, $versionSlug);
|
||||
|
||||
return (new TemplateLoader())->capture('page', [
|
||||
'post' => $post,
|
||||
'product' => $product,
|
||||
'product_item' => $productItem,
|
||||
'version' => $version,
|
||||
'versions' => $this->versionsForProduct($productSlug),
|
||||
'nav_tree' => is_array($navTree) ? $navTree : [],
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'product_slug' => $productSlug,
|
||||
'version_slug' => $versionSlug,
|
||||
'active_page_slug' => $activePageSlug,
|
||||
'page_link_slugs' => $this->pageLinkSlugs($pages, $productSlug),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
]);
|
||||
}
|
||||
@@ -316,18 +350,33 @@ final class Router
|
||||
private function captureShell(string $content, string $productSlug = '', string $versionSlug = '', string $pageSlug = ''): string
|
||||
{
|
||||
$activePages = [];
|
||||
$activeProduct = self::frontendProduct($productSlug);
|
||||
|
||||
if ($activeProduct && isset($activeProduct['term']->slug)) {
|
||||
$productSlug = (string) $activeProduct['term']->slug;
|
||||
}
|
||||
|
||||
if ($productSlug && $versionSlug) {
|
||||
$activePages = $this->pagesForVersion($productSlug, $versionSlug);
|
||||
|
||||
if ('' === $pageSlug) {
|
||||
$landing = $this->landingPageForVersion($productSlug, $versionSlug);
|
||||
if ($landing) {
|
||||
$landingSlugs = $this->pageLinkSlugs([$landing], $productSlug);
|
||||
$pageSlug = (string) ($landingSlugs[$landing->ID] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (new TemplateLoader())->capture('docs-app', [
|
||||
'content' => $content,
|
||||
'products' => self::productsWithVersions(),
|
||||
'active_product' => $activeProduct,
|
||||
'active_product_slug' => $productSlug,
|
||||
'active_version_slug' => $versionSlug,
|
||||
'active_page_slug' => $pageSlug,
|
||||
'active_pages' => $activePages,
|
||||
'active_page_link_slugs' => $this->pageLinkSlugs($activePages, $productSlug),
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
]);
|
||||
@@ -356,37 +405,93 @@ final class Router
|
||||
public static function productsWithVersions(): array
|
||||
{
|
||||
$products = get_terms(['taxonomy' => 'kb_product', 'hide_empty' => false]);
|
||||
$items = [];
|
||||
$groups = [];
|
||||
$repository = new ProductRepository();
|
||||
|
||||
if (is_wp_error($products)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($products as $product) {
|
||||
$versions = (new self())->versionsForProduct($product->slug);
|
||||
$meta = $repository->frontendMeta($product);
|
||||
$groupSlug = (string) $meta['group_slug'];
|
||||
|
||||
if (! $versions) {
|
||||
continue;
|
||||
if (! isset($groups[$groupSlug])) {
|
||||
$groups[$groupSlug] = [
|
||||
'term' => (object) [
|
||||
'term_id' => 0,
|
||||
'name' => (string) $meta['group_name'],
|
||||
'slug' => $groupSlug,
|
||||
],
|
||||
'source_terms' => [],
|
||||
'parts' => [],
|
||||
'category' => (string) $meta['category'],
|
||||
'versions' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
$groups[$groupSlug]['source_terms'][] = $product;
|
||||
$groups[$groupSlug]['parts'][$product->slug] = [
|
||||
'term' => $product,
|
||||
'versions' => $versions,
|
||||
'label' => (string) ($meta['part_label'] ?: $product->name),
|
||||
'category' => (string) $meta['category'],
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
$router = new self();
|
||||
foreach ($groups as $groupSlug => &$group) {
|
||||
$group['versions'] = $router->versionsForProduct((string) $groupSlug);
|
||||
}
|
||||
unset($group);
|
||||
|
||||
$groups = array_values(array_filter($groups, static fn (array $group): bool => ! empty($group['versions'])));
|
||||
usort($groups, static function (array $a, array $b): int {
|
||||
$categoryCompare = strcasecmp((string) ($a['category'] ?? ''), (string) ($b['category'] ?? ''));
|
||||
return 0 !== $categoryCompare ? $categoryCompare : strcasecmp((string) $a['term']->name, (string) $b['term']->name);
|
||||
});
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public static function frontendProduct(string $productSlug): ?array
|
||||
{
|
||||
$items = self::productsWithVersions();
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($productSlug === (string) $item['term']->slug) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
$term = get_term_by('slug', $productSlug, 'kb_product');
|
||||
if ($term instanceof \WP_Term) {
|
||||
$meta = (new ProductRepository())->frontendMeta($term);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ((string) $meta['group_slug'] === (string) $item['term']->slug) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function versionsForProduct(string $productSlug): array
|
||||
{
|
||||
$productSlugs = $this->sourceProductSlugs($productSlug);
|
||||
|
||||
if (! $productSlugs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = new \WP_Query([
|
||||
'post_type' => 'kb_doc_page',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'tax_query' => [
|
||||
['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlug],
|
||||
['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlugs],
|
||||
],
|
||||
]);
|
||||
$versions = [];
|
||||
@@ -404,6 +509,12 @@ final class Router
|
||||
|
||||
private function pagesForVersion(string $productSlug, string $versionSlug): array
|
||||
{
|
||||
$productSlugs = $this->sourceProductSlugs($productSlug);
|
||||
|
||||
if (! $productSlugs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = new \WP_Query([
|
||||
'post_type' => 'kb_doc_page',
|
||||
'post_status' => 'publish',
|
||||
@@ -412,7 +523,7 @@ final class Router
|
||||
'orderby' => ['meta_value_num' => 'ASC', 'title' => 'ASC'],
|
||||
'tax_query' => [
|
||||
'relation' => 'AND',
|
||||
['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlug],
|
||||
['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlugs],
|
||||
['taxonomy' => 'kb_version', 'field' => 'slug', 'terms' => $versionSlug],
|
||||
],
|
||||
]);
|
||||
@@ -423,7 +534,7 @@ final class Router
|
||||
private function landingPageForVersion(string $productSlug, string $versionSlug): ?\WP_Post
|
||||
{
|
||||
$repository = new PageRepository();
|
||||
$landing = $repository->findFrontendPage($productSlug, $versionSlug, '');
|
||||
$landing = $repository->findFrontendPageInProducts($this->sourceProductSlugs($productSlug), $versionSlug, '');
|
||||
|
||||
if ($landing) {
|
||||
return $landing;
|
||||
@@ -433,4 +544,45 @@ final class Router
|
||||
|
||||
return $pages[0] ?? null;
|
||||
}
|
||||
|
||||
private function sourceProductSlugs(string $productSlug): array
|
||||
{
|
||||
$terms = get_terms(['taxonomy' => 'kb_product', 'hide_empty' => false]);
|
||||
$repository = new ProductRepository();
|
||||
$slugs = [];
|
||||
|
||||
if (is_wp_error($terms)) {
|
||||
return [$productSlug];
|
||||
}
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$meta = $repository->frontendMeta($term);
|
||||
|
||||
if ($productSlug === (string) $meta['group_slug']) {
|
||||
$slugs[] = $term->slug;
|
||||
}
|
||||
}
|
||||
|
||||
return $slugs ?: [$productSlug];
|
||||
}
|
||||
|
||||
private function pageLinkSlugs(array $pages, string $productSlug): array
|
||||
{
|
||||
$product = self::frontendProduct($productSlug);
|
||||
$sourceTerms = (array) ($product['source_terms'] ?? []);
|
||||
$multiPart = count($sourceTerms) > 1;
|
||||
$slugs = [];
|
||||
|
||||
foreach ($pages as $page) {
|
||||
if (! $page instanceof \WP_Post) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pageSlug = (string) get_post_meta($page->ID, '_kb_page_slug', true);
|
||||
$sourceSlug = (string) get_post_meta($page->ID, '_kb_product_slug', true);
|
||||
$slugs[$page->ID] = ($multiPart && '' !== $sourceSlug) ? $sourceSlug . '--' . ($pageSlug ?: 'index') : $pageSlug;
|
||||
}
|
||||
|
||||
return $slugs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace KbMarkdownImporter\Frontend;
|
||||
|
||||
use KbMarkdownImporter\Plugin;
|
||||
use KbMarkdownImporter\Repository\ProductRepository;
|
||||
|
||||
final class UrlBuilder
|
||||
{
|
||||
@@ -41,9 +42,40 @@ final class UrlBuilder
|
||||
|
||||
public static function page(string $productSlug, string $versionSlug, string $pageSlug = ''): string
|
||||
{
|
||||
[$productSlug, $pageSlug] = self::normalizeProductRoute($productSlug, $pageSlug);
|
||||
|
||||
return self::route('page', $productSlug, $versionSlug, $pageSlug);
|
||||
}
|
||||
|
||||
private static function normalizeProductRoute(string $productSlug, string $pageSlug): array
|
||||
{
|
||||
$term = get_term_by('slug', $productSlug, 'kb_product');
|
||||
|
||||
if (! $term instanceof \WP_Term) {
|
||||
return [$productSlug, $pageSlug];
|
||||
}
|
||||
|
||||
$repository = new ProductRepository();
|
||||
$meta = $repository->frontendMeta($term);
|
||||
$groupSlug = (string) $meta['group_slug'];
|
||||
$groupTermCount = 0;
|
||||
$terms = get_terms(['taxonomy' => 'kb_product', 'hide_empty' => false]);
|
||||
|
||||
if (! is_wp_error($terms)) {
|
||||
foreach ($terms as $candidate) {
|
||||
if ($groupSlug === (string) $repository->frontendMeta($candidate)['group_slug']) {
|
||||
$groupTermCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($groupTermCount > 1 && ! str_contains($pageSlug, '--')) {
|
||||
$pageSlug = $term->slug . '--' . ($pageSlug ?: 'index');
|
||||
}
|
||||
|
||||
return [$groupSlug, $pageSlug];
|
||||
}
|
||||
|
||||
private static function route(string $route, string $productSlug = '', string $versionSlug = '', string $pageSlug = ''): string
|
||||
{
|
||||
if (self::isEmbed()) {
|
||||
|
||||
397
kb-markdown-importer/includes/Olm/ChangelogSync.php
Normal file
397
kb-markdown-importer/includes/Olm/ChangelogSync.php
Normal file
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KbMarkdownImporter\Olm;
|
||||
|
||||
use KbMarkdownImporter\Import\ImportLogger;
|
||||
use KbMarkdownImporter\Plugin;
|
||||
|
||||
final class ChangelogSync
|
||||
{
|
||||
private const OPTION_ITEMS = 'kb_markdown_importer_product_updates';
|
||||
private const OPTION_LAST_SYNC = 'kb_markdown_importer_changelog_last_sync';
|
||||
|
||||
private array $settings;
|
||||
private string $baseUrl;
|
||||
private array $headers = [];
|
||||
|
||||
public function __construct(?array $settings = null)
|
||||
{
|
||||
$this->settings = $settings ?: Plugin::settings();
|
||||
$this->baseUrl = self::normalizeBaseUrl((string) ($this->settings['olm_base_url'] ?? ''));
|
||||
}
|
||||
|
||||
public static function items(): array
|
||||
{
|
||||
$items = get_option(self::OPTION_ITEMS, []);
|
||||
|
||||
return is_array($items) ? $items : [];
|
||||
}
|
||||
|
||||
public static function lastSync(): string
|
||||
{
|
||||
return (string) get_option(self::OPTION_LAST_SYNC, '');
|
||||
}
|
||||
|
||||
public static function normalizeBaseUrl(string $url): string
|
||||
{
|
||||
$url = trim($url);
|
||||
|
||||
if ('' === $url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (! preg_match('#^https?://#i', $url)) {
|
||||
$url = 'https://' . $url;
|
||||
}
|
||||
|
||||
return rtrim($url, '/');
|
||||
}
|
||||
|
||||
public function sync(): \WP_REST_Response
|
||||
{
|
||||
ImportLogger::info('OLM changelog synchronization started.');
|
||||
|
||||
$token = $this->login();
|
||||
if (is_wp_error($token)) {
|
||||
ImportLogger::error('OLM changelog login failed: ' . $token->get_error_message());
|
||||
return new \WP_REST_Response(['success' => false, 'message' => $token->get_error_message()], 500);
|
||||
}
|
||||
|
||||
$this->headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
];
|
||||
|
||||
$downloadIds = $this->productDownloadIds();
|
||||
if (is_wp_error($downloadIds)) {
|
||||
ImportLogger::error('OLM product lookup failed: ' . $downloadIds->get_error_message());
|
||||
return new \WP_REST_Response(['success' => false, 'message' => $downloadIds->get_error_message()], 500);
|
||||
}
|
||||
|
||||
$items = [];
|
||||
|
||||
foreach ($downloadIds as $downloadId) {
|
||||
$versions = $this->downloadFieldVersions($downloadId);
|
||||
|
||||
if (is_wp_error($versions)) {
|
||||
ImportLogger::warning('OLM download field lookup failed for ' . $downloadId . ': ' . $versions->get_error_message());
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($versions as $version) {
|
||||
$item = $this->normalizeVersion($version);
|
||||
|
||||
if (null !== $item) {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($items, static fn (array $a, array $b): int => ($b['_timestamp'] ?? 0) <=> ($a['_timestamp'] ?? 0));
|
||||
$items = array_filter($items, fn (array $item): bool => $this->isInDateWindow($item));
|
||||
$items = array_map(static function (array $item): array {
|
||||
unset($item['_timestamp']);
|
||||
return $item;
|
||||
}, array_values($items));
|
||||
|
||||
update_option(self::OPTION_ITEMS, $items, false);
|
||||
update_option(self::OPTION_LAST_SYNC, current_time('mysql'), false);
|
||||
ImportLogger::info('OLM changelog synchronization completed. Entries: ' . count($items));
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'stats' => [
|
||||
'downloads' => count($downloadIds),
|
||||
'updates' => count($items),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function login(): string|\WP_Error
|
||||
{
|
||||
if ('' === $this->baseUrl) {
|
||||
return new \WP_Error('kb_olm_missing_base_url', __('OLM base URL missing.', 'kb-markdown-importer'));
|
||||
}
|
||||
|
||||
$username = trim((string) ($this->settings['olm_username'] ?? ''));
|
||||
$password = (string) ($this->settings['olm_password'] ?? '');
|
||||
|
||||
if ('' === $username || '' === $password) {
|
||||
return new \WP_Error('kb_olm_missing_credentials', __('OLM credentials missing.', 'kb-markdown-importer'));
|
||||
}
|
||||
|
||||
$response = wp_remote_post($this->baseUrl . '/login', [
|
||||
'timeout' => 12,
|
||||
'redirection' => 3,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
]),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status = (int) wp_remote_retrieve_response_code($response);
|
||||
$body = (string) wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if ($status < 200 || $status >= 300 || ! is_array($data) || empty($data['bearerToken'])) {
|
||||
return new \WP_Error(
|
||||
'kb_olm_login_failed',
|
||||
__('OLM login failed.', 'kb-markdown-importer'),
|
||||
[
|
||||
'status' => $status,
|
||||
'response_excerpt' => substr(wp_strip_all_tags($body), 0, 1000),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return (string) $data['bearerToken'];
|
||||
}
|
||||
|
||||
private function productDownloadIds(): array|\WP_Error
|
||||
{
|
||||
$ids = [];
|
||||
$page = 1;
|
||||
|
||||
while (true) {
|
||||
$data = $this->getJson($this->baseUrl . '/api/rest/v1/product?page=' . $page . '&size=1');
|
||||
|
||||
if (is_wp_error($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$products = is_array($data['content'] ?? null) ? $data['content'] : [];
|
||||
if (! $products) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($products as $product) {
|
||||
if (! is_array($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((array) ($product['downloads'] ?? []) as $download) {
|
||||
$id = is_array($download) ? (string) ($download['id'] ?? '') : '';
|
||||
|
||||
if ('' !== $id) {
|
||||
$ids[$id] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$page++;
|
||||
}
|
||||
|
||||
return array_values($ids);
|
||||
}
|
||||
|
||||
private function downloadFieldVersions(string $downloadId): array|\WP_Error
|
||||
{
|
||||
$versions = [];
|
||||
$page = 1;
|
||||
|
||||
while (true) {
|
||||
$data = $this->getJson($this->baseUrl . '/api/rest/v1/download/field/' . rawurlencode($downloadId) . '?page=' . $page . '&size=1');
|
||||
|
||||
if (is_wp_error($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$content = is_array($data['content'] ?? null) ? $data['content'] : [];
|
||||
if (! $content) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($content as $version) {
|
||||
if (is_array($version)) {
|
||||
$versions[] = $version;
|
||||
}
|
||||
}
|
||||
|
||||
$page++;
|
||||
}
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
private function getJson(string $url): array|\WP_Error
|
||||
{
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => 12,
|
||||
'redirection' => 3,
|
||||
'headers' => $this->headers,
|
||||
'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status = (int) wp_remote_retrieve_response_code($response);
|
||||
$body = (string) wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if ($status < 200 || $status >= 300 || ! is_array($data)) {
|
||||
return new \WP_Error(
|
||||
'kb_olm_request_failed',
|
||||
__('OLM request failed.', 'kb-markdown-importer'),
|
||||
[
|
||||
'status' => $status,
|
||||
'url' => $url,
|
||||
'response_excerpt' => substr(wp_strip_all_tags($body), 0, 1000),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function normalizeVersion(array $version): ?array
|
||||
{
|
||||
$productVersion = is_array($version['productVersion'] ?? null) ? $version['productVersion'] : [];
|
||||
$product = is_array($productVersion['product'] ?? null) ? $productVersion['product'] : [];
|
||||
$downloadField = is_array($version['downloadField'] ?? null) ? $version['downloadField'] : [];
|
||||
$productNo = strtolower((string) ($product['productNo'] ?? ''));
|
||||
$publishedAt = (string) ($version['publishedAt'] ?? '');
|
||||
|
||||
if (true !== ($version['qa'] ?? false) || true !== ($product['published'] ?? false) || '' === $publishedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($productNo, $this->ignoredOlmNumbers(), true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timestamp = $this->dateTimestamp($publishedAt);
|
||||
$productName = $this->cleanText((string) ($product['name'] ?? ''));
|
||||
$downloadName = $this->cleanText((string) ($downloadField['name'] ?? ''));
|
||||
|
||||
if (true !== ($downloadField['starfaceModule'] ?? false) && '' !== $downloadName) {
|
||||
$productName = trim($productName . ' - ' . $downloadName);
|
||||
}
|
||||
|
||||
$changelogLines = $this->changelogLines((string) ($version['changelog'] ?? ''));
|
||||
|
||||
return [
|
||||
'product' => $productName,
|
||||
'version' => $this->moduleVersion($productVersion, $version),
|
||||
'date' => $this->formatDate($publishedAt),
|
||||
'month_label' => $timestamp > 0 ? wp_date('F Y', $timestamp) : '',
|
||||
'changelog' => implode(' ', $changelogLines),
|
||||
'changelog_lines' => $changelogLines,
|
||||
'starface_min' => $this->starfaceVersion(is_array($productVersion['minStarfaceVersion'] ?? null) ? $productVersion['minStarfaceVersion'] : []),
|
||||
'starface_max' => $this->starfaceVersion(is_array($productVersion['maxStarfaceVersion'] ?? null) ? $productVersion['maxStarfaceVersion'] : []),
|
||||
'link' => $this->validUrl((string) ($product['productPageURI'] ?? '')),
|
||||
'download_link' => '' !== $productNo ? 'https://get.o-byte.com?olm=' . rawurlencode($productNo) : '',
|
||||
'product_no' => $productNo,
|
||||
'published_at' => $timestamp > 0 ? wp_date('Y-m-d', $timestamp) : '',
|
||||
'_timestamp' => $timestamp,
|
||||
];
|
||||
}
|
||||
|
||||
private function isInDateWindow(array $item): bool
|
||||
{
|
||||
$timestamp = (int) ($item['_timestamp'] ?? 0);
|
||||
if ($timestamp <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$months = max(1, min(24, (int) ($this->settings['product_updates_olm_months'] ?? 4)));
|
||||
$timezone = wp_timezone();
|
||||
$date = (new \DateTimeImmutable('@' . $timestamp))->setTimezone($timezone);
|
||||
$now = new \DateTimeImmutable('now', $timezone);
|
||||
$start = $now->modify('first day of this month')->modify('-' . $months . ' months')->setTime(0, 0, 0);
|
||||
|
||||
return $date >= $start && $date <= $now;
|
||||
}
|
||||
|
||||
private function ignoredOlmNumbers(): array
|
||||
{
|
||||
$value = strtolower((string) ($this->settings['product_updates_olm_ignore_numbers'] ?? ''));
|
||||
|
||||
return array_values(array_filter(array_map('trim', explode(',', $value)), static fn (string $item): bool => '' !== $item));
|
||||
}
|
||||
|
||||
private function moduleVersion(array $productVersion, array $downloadVersion): string
|
||||
{
|
||||
return implode('.', [
|
||||
(string) ($productVersion['major'] ?? ''),
|
||||
(string) ($productVersion['minor'] ?? ''),
|
||||
(string) ($downloadVersion['bugfixVersion'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
private function starfaceVersion(array $version): string
|
||||
{
|
||||
return implode('.', [
|
||||
(string) ($version['major'] ?? ''),
|
||||
(string) ($version['minor'] ?? ''),
|
||||
(string) ($version['build'] ?? ''),
|
||||
(string) ($version['revision'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
private function changelogLines(string $value): array
|
||||
{
|
||||
$value = str_replace(["\r\n", "\r"], "\n", $value);
|
||||
$lines = [];
|
||||
|
||||
foreach (explode("\n", $value) as $line) {
|
||||
$line = $this->cleanText($line);
|
||||
|
||||
if ('' !== $line) {
|
||||
$lines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function cleanText(string $value): string
|
||||
{
|
||||
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, get_bloginfo('charset'));
|
||||
$value = preg_replace('/<br\s*\/?>/i', ' ', $value) ?? $value;
|
||||
$value = wp_strip_all_tags($value);
|
||||
$value = preg_replace('/\s+/', ' ', $value) ?? $value;
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
private function validUrl(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if ('' === $value || in_array($value, ['.', '-'], true)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return filter_var($value, FILTER_VALIDATE_URL) ? $value : '';
|
||||
}
|
||||
|
||||
private function dateTimestamp(string $date): int
|
||||
{
|
||||
if ('' === $date) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($date);
|
||||
|
||||
return $timestamp ?: 0;
|
||||
}
|
||||
|
||||
private function formatDate(string $date): string
|
||||
{
|
||||
$timestamp = $this->dateTimestamp($date);
|
||||
|
||||
return $timestamp > 0 ? wp_date((string) get_option('date_format'), $timestamp) : $date;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use KbMarkdownImporter\Admin\SyncPage;
|
||||
use KbMarkdownImporter\Frontend\Router;
|
||||
use KbMarkdownImporter\Frontend\SearchController;
|
||||
use KbMarkdownImporter\Import\ImportManager;
|
||||
use KbMarkdownImporter\Olm\ChangelogSync;
|
||||
|
||||
final class Plugin
|
||||
{
|
||||
@@ -135,7 +136,15 @@ final class Plugin
|
||||
|
||||
register_rest_route('kb-markdown/v1', '/sync', [
|
||||
'methods' => 'POST',
|
||||
'callback' => static fn (\WP_REST_Request $request): \WP_REST_Response => (new ImportManager())->syncAll((bool) $request->get_param('dry_run')),
|
||||
'callback' => static function (\WP_REST_Request $request): \WP_REST_Response {
|
||||
$response = (new ImportManager())->syncAll((bool) $request->get_param('dry_run'));
|
||||
|
||||
if (! (bool) $request->get_param('dry_run')) {
|
||||
(new ChangelogSync())->sync();
|
||||
}
|
||||
|
||||
return $response;
|
||||
},
|
||||
'permission_callback' => static fn (): bool => current_user_can('sync_kb_docs'),
|
||||
]);
|
||||
|
||||
@@ -145,6 +154,12 @@ final class Plugin
|
||||
'permission_callback' => static fn (): bool => current_user_can('sync_kb_docs'),
|
||||
]);
|
||||
|
||||
register_rest_route('kb-markdown/v1', '/sync/changelog', [
|
||||
'methods' => 'POST',
|
||||
'callback' => static fn (): \WP_REST_Response => (new ChangelogSync())->sync(),
|
||||
'permission_callback' => static fn (): bool => current_user_can('sync_kb_docs'),
|
||||
]);
|
||||
|
||||
register_rest_route('kb-markdown/v1', '/search', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [SearchController::class, 'restSearch'],
|
||||
@@ -179,6 +194,7 @@ final class Plugin
|
||||
public function runCronSync(): void
|
||||
{
|
||||
(new ImportManager())->syncAll(false);
|
||||
(new ChangelogSync())->sync();
|
||||
}
|
||||
|
||||
public function enqueueFrontendAssets(): void
|
||||
|
||||
@@ -111,7 +111,18 @@ final class PageRepository
|
||||
|
||||
public function findFrontendPage(string $productSlug, string $versionSlug, string $pageSlug): ?\WP_Post
|
||||
{
|
||||
return $this->findFrontendPageInProducts([$productSlug], $versionSlug, $pageSlug);
|
||||
}
|
||||
|
||||
public function findFrontendPageInProducts(array $productSlugs, string $versionSlug, string $pageSlug): ?\WP_Post
|
||||
{
|
||||
$productSlugs = array_values(array_filter(array_map('sanitize_title', $productSlugs)));
|
||||
$pageSlug = $pageSlug ?: '';
|
||||
|
||||
if (! $productSlugs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = new \WP_Query([
|
||||
'post_type' => 'kb_doc_page',
|
||||
'post_status' => 'publish',
|
||||
@@ -119,10 +130,12 @@ final class PageRepository
|
||||
'no_found_rows' => true,
|
||||
'meta_query' => [
|
||||
'relation' => 'AND',
|
||||
['key' => '_kb_product_slug', 'value' => $productSlug],
|
||||
['key' => '_kb_version_slug', 'value' => $versionSlug],
|
||||
['key' => '_kb_page_slug', 'value' => $pageSlug],
|
||||
],
|
||||
'tax_query' => [
|
||||
['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlugs],
|
||||
],
|
||||
]);
|
||||
|
||||
return $query->have_posts() ? $query->posts[0] : null;
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace KbMarkdownImporter\Repository;
|
||||
|
||||
final class ProductRepository
|
||||
{
|
||||
public const META_GROUP_NAME = '_kb_product_group_name';
|
||||
public const META_GROUP_SLUG = '_kb_product_group_slug';
|
||||
public const META_PART_LABEL = '_kb_product_part_label';
|
||||
public const META_CATEGORY = '_kb_product_category';
|
||||
|
||||
public function ensure(string $name, string $slug = ''): int
|
||||
{
|
||||
$slug = $slug ? sanitize_title($slug) : sanitize_title($name);
|
||||
@@ -54,13 +59,14 @@ final class ProductRepository
|
||||
'term' => $term,
|
||||
'page_count' => count($pageIds),
|
||||
'versions' => array_values($versions),
|
||||
'meta' => $this->frontendMeta($term),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function update(int $termId, string $name, string $slug): \WP_Term|\WP_Error
|
||||
public function update(int $termId, string $name, string $slug, array $frontend = []): \WP_Term|\WP_Error
|
||||
{
|
||||
$name = trim($name);
|
||||
$slug = sanitize_title($slug ?: $name);
|
||||
@@ -82,6 +88,23 @@ final class ProductRepository
|
||||
update_post_meta($pageId, '_kb_product_slug', $slug);
|
||||
}
|
||||
|
||||
$groupName = sanitize_text_field((string) ($frontend['group_name'] ?? ''));
|
||||
$groupName = '' !== trim($groupName) ? $groupName : $name;
|
||||
$groupSlugInput = sanitize_title((string) ($frontend['group_slug'] ?? ''));
|
||||
$groupSlug = $groupSlugInput ?: sanitize_title($groupName);
|
||||
|
||||
if ($groupSlugInput === $slug && 0 !== strcasecmp($groupName, $name)) {
|
||||
$groupSlug = sanitize_title($groupName);
|
||||
}
|
||||
|
||||
$partLabel = sanitize_text_field((string) ($frontend['part_label'] ?? ''));
|
||||
$category = sanitize_text_field((string) ($frontend['category'] ?? ''));
|
||||
|
||||
update_term_meta($termId, self::META_GROUP_NAME, $groupName ?: $name);
|
||||
update_term_meta($termId, self::META_GROUP_SLUG, $groupSlug ?: $slug);
|
||||
update_term_meta($termId, self::META_PART_LABEL, $partLabel);
|
||||
update_term_meta($termId, self::META_CATEGORY, $category);
|
||||
|
||||
$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'));
|
||||
@@ -115,6 +138,27 @@ final class ProductRepository
|
||||
return true;
|
||||
}
|
||||
|
||||
public function frontendMeta(\WP_Term $term): array
|
||||
{
|
||||
$storedGroupName = trim((string) get_term_meta($term->term_id, self::META_GROUP_NAME, true));
|
||||
$storedGroupSlug = sanitize_title((string) get_term_meta($term->term_id, self::META_GROUP_SLUG, true));
|
||||
$groupName = '' !== $storedGroupName ? $storedGroupName : $term->name;
|
||||
$groupSlug = '' !== $storedGroupSlug ? $storedGroupSlug : ('' !== $storedGroupName ? sanitize_title($groupName) : $term->slug);
|
||||
$partLabel = trim((string) get_term_meta($term->term_id, self::META_PART_LABEL, true));
|
||||
$category = trim((string) get_term_meta($term->term_id, self::META_CATEGORY, true));
|
||||
|
||||
if ('' !== $storedGroupName && $groupSlug === $term->slug && 0 !== strcasecmp($groupName, $term->name)) {
|
||||
$groupSlug = sanitize_title($groupName);
|
||||
}
|
||||
|
||||
return [
|
||||
'group_name' => $groupName,
|
||||
'group_slug' => '' !== $groupSlug ? $groupSlug : $term->slug,
|
||||
'part_label' => $partLabel,
|
||||
'category' => $category,
|
||||
];
|
||||
}
|
||||
|
||||
private function pageIdsForProduct(int $termId, array $postStatus = ['publish']): array
|
||||
{
|
||||
$query = new \WP_Query([
|
||||
|
||||
@@ -24,20 +24,12 @@ final class Settings
|
||||
'custom_theme_css_url' => '',
|
||||
'docs_home_intro_title' => 'So nutzt du die Dokumentation',
|
||||
'docs_home_intro_content' => '<p>Wähle links ein Produkt aus und öffne anschließend die passende Version. Innerhalb einer Version findest du die zugehörigen Seiten der Dokumentation in der Navigation.</p>',
|
||||
'product_updates_source' => 'rss',
|
||||
'product_updates_feed_url' => '',
|
||||
'product_updates_feed_limit' => '5',
|
||||
'product_updates_feed_item_path' => 'channel/item',
|
||||
'product_updates_feed_product_field' => 'title',
|
||||
'product_updates_feed_version_field' => 'category',
|
||||
'product_updates_feed_date_field' => 'pubDate',
|
||||
'product_updates_feed_changelog_field' => 'description',
|
||||
'product_updates_rest_url' => '',
|
||||
'product_updates_rest_list_path' => 'content,data,items',
|
||||
'product_updates_rest_product_field' => 'product.name,productName,name',
|
||||
'product_updates_rest_version_field' => 'version,versionName,name',
|
||||
'product_updates_rest_date_field' => 'releaseDate,date,updatedAt,createdAt',
|
||||
'product_updates_rest_changelog_field' => 'changelog,changeLog,description,changes',
|
||||
'product_updates_source' => 'olm_changelog',
|
||||
'product_updates_olm_months' => '4',
|
||||
'product_updates_olm_ignore_numbers' => 'olm-10109,olm-10110',
|
||||
'olm_base_url' => 'https://olm.o-byte.com',
|
||||
'olm_username' => '',
|
||||
'olm_password' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,27 @@ Optional:
|
||||
- WordPress-native Markdown rendering with internal `.md` link rewriting.
|
||||
- Frontend routes under `/docs/`.
|
||||
- Shortcodes: `[kb_docs]`, `[kb_docs_index]`, `[kb_product_index product="..."]`, `[kb_search]`.
|
||||
- Stored OLM changelog sync using the saved OLM credentials, grouped by release month.
|
||||
- Product management for frontend product grouping, documentation parts and categories.
|
||||
- Wide documentation app layout with an always-visible active page navigation.
|
||||
- Import logs without exposing secrets.
|
||||
|
||||
## Product grouping
|
||||
|
||||
Imported GitLab projects stay as individual `kb_product` terms. In **Knowledgebase > Products**, each imported term can be assigned to a shared frontend product by setting the same frontend product name and slug. Changes are edited in one table and persisted with **Save all products**.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
HubSpot App -> frontend product: HubSpot, part: App
|
||||
HubSpot Modul -> frontend product: HubSpot, part: Modul
|
||||
DATEV Exporter -> frontend product: DATEV, part: Exporter
|
||||
```
|
||||
|
||||
Products can also be assigned a category such as `CRM`, `Telefonie` or `Integrationen`; the frontend groups product cards and sidebar entries by this category.
|
||||
|
||||
## OLM changelog sync
|
||||
|
||||
The changelog is synchronized manually from **Knowledgebase > Synchronization** with `Sync OLM Changelog`, or automatically whenever the configured documentation sync interval runs. `Sync All` runs both the GitLab import and the OLM changelog sync.
|
||||
|
||||
The sync follows the existing Python flow: login, load OLM products, collect download IDs, load download field versions, then store only QA-approved published changelog entries in WordPress. The documentation overview reads this stored data instead of calling OLM on every page view.
|
||||
|
||||
@@ -4,6 +4,14 @@ defined('ABSPATH') || exit;
|
||||
$active_product_slug = (string) ($active_product_slug ?? '');
|
||||
$active_version_slug = (string) ($active_version_slug ?? '');
|
||||
$active_page_slug = (string) ($active_page_slug ?? '');
|
||||
$active_product = is_array($active_product ?? null) ? $active_product : null;
|
||||
$active_page_link_slugs = (array) ($active_page_link_slugs ?? []);
|
||||
$products_by_category = [];
|
||||
|
||||
foreach ((array) $products as $item) {
|
||||
$category = trim((string) ($item['category'] ?? ''));
|
||||
$products_by_category[$category ?: __('Weitere Produkte', 'kb-markdown-importer')][] = $item;
|
||||
}
|
||||
?>
|
||||
<div class="kb-docs-wrap kb-docs-app">
|
||||
<aside class="kb-app-sidebar" aria-label="<?php esc_attr_e('Dokumentationsnavigation', 'kb-markdown-importer'); ?>">
|
||||
@@ -13,31 +21,49 @@ $active_page_slug = (string) ($active_page_slug ?? '');
|
||||
<div class="kb-app-sidebar__search">
|
||||
<?php echo do_shortcode('[kb_search]'); ?>
|
||||
</div>
|
||||
<?php if ($active_product && $active_version_slug && ! empty($active_pages)) : ?>
|
||||
<section class="kb-app-active-nav" aria-label="<?php esc_attr_e('Aktive Dokumentation', 'kb-markdown-importer'); ?>">
|
||||
<a class="kb-app-active-nav__title" href="<?php echo esc_url($url_builder::version($active_product_slug, $active_version_slug)); ?>">
|
||||
<?php echo esc_html((string) $active_product['term']->name); ?>
|
||||
</a>
|
||||
<ul class="kb-app-page-list">
|
||||
<?php foreach ((array) $active_pages as $page) : ?>
|
||||
<?php
|
||||
$linkSlug = (string) ($active_page_link_slugs[$page->ID] ?? get_post_meta($page->ID, '_kb_page_slug', true));
|
||||
$isActivePage = $linkSlug === $active_page_slug || ('' === $active_page_slug && in_array($linkSlug, ['', 'index'], true));
|
||||
$sourceSlug = (string) get_post_meta($page->ID, '_kb_product_slug', true);
|
||||
$part = (array) (($active_product['parts'][$sourceSlug] ?? []) ?: []);
|
||||
?>
|
||||
<li class="<?php echo $isActivePage ? 'is-active' : ''; ?>">
|
||||
<a href="<?php echo esc_url($url_builder::page($active_product_slug, $active_version_slug, $linkSlug)); ?>">
|
||||
<?php if (! empty($part['label']) && count((array) $active_product['parts']) > 1) : ?>
|
||||
<span class="kb-app-page-list__part"><?php echo esc_html((string) $part['label']); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php echo esc_html(get_the_title($page)); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<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 ($isActiveProduct && $active_version_slug && ! 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, $active_version_slug, $pageSlug)); ?>"><?php echo esc_html(get_the_title($page)); ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<h2 class="kb-app-nav__title"><?php esc_html_e('Alle Produkte', 'kb-markdown-importer'); ?></h2>
|
||||
<?php foreach ($products_by_category as $category => $category_products) : ?>
|
||||
<section class="kb-app-category">
|
||||
<h3><?php echo esc_html((string) $category); ?></h3>
|
||||
<?php foreach ((array) $category_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>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
@@ -3,6 +3,18 @@ defined('ABSPATH') || exit;
|
||||
|
||||
$settings = (array) ($settings ?? []);
|
||||
$updates = (array) ($updates ?? []);
|
||||
$updates_by_month = [];
|
||||
$products_by_category = [];
|
||||
|
||||
foreach ($updates as $update) {
|
||||
$month = (string) ($update['month_label'] ?? '');
|
||||
$updates_by_month[$month ?: ''][] = $update;
|
||||
}
|
||||
|
||||
foreach ((array) ($products ?? []) as $item) {
|
||||
$category = trim((string) ($item['category'] ?? ''));
|
||||
$products_by_category[$category ?: __('Weitere Produkte', 'kb-markdown-importer')][] = $item;
|
||||
}
|
||||
?>
|
||||
<section class="kb-docs-home">
|
||||
<h1><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></h1>
|
||||
@@ -16,38 +28,85 @@ $updates = (array) ($updates ?? []);
|
||||
<section class="kb-home-card kb-product-updates">
|
||||
<h2><?php esc_html_e('Neueste Produktupdates', 'kb-markdown-importer'); ?></h2>
|
||||
<?php if ($updates) : ?>
|
||||
<ul class="kb-product-updates__list">
|
||||
<?php foreach ($updates as $update) : ?>
|
||||
<li>
|
||||
<div class="kb-product-updates__meta">
|
||||
<strong><?php echo esc_html((string) ($update['product'] ?: __('Produkt', 'kb-markdown-importer'))); ?></strong>
|
||||
<?php if (! empty($update['version'])) : ?><span><?php echo esc_html((string) $update['version']); ?></span><?php endif; ?>
|
||||
<?php if (! empty($update['date'])) : ?><time><?php echo esc_html((string) $update['date']); ?></time><?php endif; ?>
|
||||
</div>
|
||||
<?php if (! empty($update['changelog'])) : ?>
|
||||
<p><?php echo esc_html((string) $update['changelog']); ?></p>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php foreach ($updates_by_month as $month => $month_updates) : ?>
|
||||
<section class="kb-product-updates__month">
|
||||
<?php if ('' !== $month) : ?><h3><?php echo esc_html($month); ?></h3><?php endif; ?>
|
||||
<ul class="kb-product-updates__list">
|
||||
<?php foreach ($month_updates as $update) : ?>
|
||||
<li>
|
||||
<div class="kb-product-updates__meta">
|
||||
<?php if (! empty($update['link'])) : ?>
|
||||
<strong><a href="<?php echo esc_url((string) $update['link']); ?>" target="_blank" rel="noopener"><?php echo esc_html((string) ($update['product'] ?: __('Produkt', 'kb-markdown-importer'))); ?></a></strong>
|
||||
<?php else : ?>
|
||||
<strong><?php echo esc_html((string) ($update['product'] ?: __('Produkt', 'kb-markdown-importer'))); ?></strong>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($update['version'])) : ?><span><?php echo esc_html((string) $update['version']); ?></span><?php endif; ?>
|
||||
<?php if (! empty($update['date'])) : ?><time><?php echo esc_html((string) $update['date']); ?></time><?php endif; ?>
|
||||
</div>
|
||||
<?php if (! empty($update['changelog_lines']) && is_array($update['changelog_lines'])) : ?>
|
||||
<ul class="kb-product-updates__changes">
|
||||
<?php foreach ($update['changelog_lines'] as $line) : ?>
|
||||
<li><?php echo esc_html((string) $line); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php elseif (! empty($update['changelog'])) : ?>
|
||||
<p><?php echo esc_html((string) $update['changelog']); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($update['starface_min']) || ! empty($update['starface_max']) || ! empty($update['link']) || ! empty($update['download_link'])) : ?>
|
||||
<dl class="kb-product-updates__details">
|
||||
<?php if (! empty($update['starface_min']) || ! empty($update['starface_max'])) : ?>
|
||||
<div>
|
||||
<dt><?php esc_html_e('STARFACE', 'kb-markdown-importer'); ?></dt>
|
||||
<dd><?php echo esc_html(trim((string) ($update['starface_min'] ?? '') . ' - ' . (string) ($update['starface_max'] ?? ''), ' -')); ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($update['link'])) : ?>
|
||||
<div>
|
||||
<dt><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></dt>
|
||||
<dd><a href="<?php echo esc_url((string) $update['link']); ?>" target="_blank" rel="noopener"><?php echo esc_html((string) $update['link']); ?></a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($update['download_link'])) : ?>
|
||||
<div>
|
||||
<dt><?php esc_html_e('Download', 'kb-markdown-importer'); ?></dt>
|
||||
<dd><a href="<?php echo esc_url((string) $update['download_link']); ?>" target="_blank" rel="noopener">get.o-byte.com</a></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else : ?>
|
||||
<p class="kb-empty-state"><?php esc_html_e('Es wurden noch keine Produktupdates gefunden.', 'kb-markdown-importer'); ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
<div class="kb-product-list">
|
||||
<?php foreach ((array) $products as $item) : ?>
|
||||
<?php $term = $item['term']; ?>
|
||||
<?php $latest = $item['versions'][0] ?? null; ?>
|
||||
<section class="kb-product-card">
|
||||
<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'])) : ?>
|
||||
<ul>
|
||||
<?php foreach ($item['versions'] as $version) : ?>
|
||||
<li><a href="<?php echo esc_url($url_builder::version($term->slug, $version->slug)); ?>"><?php echo esc_html($version->name); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<div class="kb-product-groups">
|
||||
<?php foreach ($products_by_category as $category => $category_products) : ?>
|
||||
<section class="kb-product-category">
|
||||
<h2><?php echo esc_html((string) $category); ?></h2>
|
||||
<div class="kb-product-list">
|
||||
<?php foreach ((array) $category_products as $item) : ?>
|
||||
<?php $term = $item['term']; ?>
|
||||
<?php $latest = $item['versions'][0] ?? null; ?>
|
||||
<section class="kb-product-card">
|
||||
<h3><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></h3>
|
||||
<?php if (! empty($item['parts']) && count((array) $item['parts']) > 1) : ?>
|
||||
<p class="kb-product-card__parts"><?php echo esc_html(implode(', ', array_map(static fn (array $part): string => (string) $part['label'], (array) $item['parts']))); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($item['versions'])) : ?>
|
||||
<ul>
|
||||
<?php foreach ($item['versions'] as $version) : ?>
|
||||
<li><a href="<?php echo esc_url($url_builder::version($term->slug, $version->slug)); ?>"><?php echo esc_html($version->name); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$product_item = is_array($product_item ?? null) ? $product_item : [];
|
||||
$parts = (array) ($product_item['parts'] ?? []);
|
||||
?>
|
||||
<section class="kb-docs-product">
|
||||
<nav class="kb-breadcrumbs"><a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></a><span>/</span><?php echo esc_html($product->name); ?></nav>
|
||||
<h1><?php echo esc_html($product->name); ?></h1>
|
||||
<h2><?php esc_html_e('Verfügbare Versionen', 'kb-markdown-importer'); ?></h2>
|
||||
<?php if (count($parts) > 1) : ?>
|
||||
<p class="kb-product-parts"><?php echo esc_html(implode(', ', array_map(static fn (array $part): string => (string) $part['label'], $parts))); ?></p>
|
||||
<?php endif; ?>
|
||||
<h2><?php esc_html_e('Verfuegbare Versionen', 'kb-markdown-importer'); ?></h2>
|
||||
<ul class="kb-version-list">
|
||||
<?php foreach ((array) $versions as $index => $version) : ?>
|
||||
<li>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
$product_item = is_array($product_item ?? null) ? $product_item : [];
|
||||
$parts = (array) ($product_item['parts'] ?? []);
|
||||
$page_link_slugs = (array) ($page_link_slugs ?? []);
|
||||
?>
|
||||
<section class="kb-docs-version">
|
||||
<header class="kb-doc-header">
|
||||
@@ -26,8 +29,19 @@ defined('ABSPATH') || exit;
|
||||
</header>
|
||||
<ul class="kb-page-list">
|
||||
<?php foreach ((array) $pages as $page) : ?>
|
||||
<?php $slug = (string) get_post_meta($page->ID, '_kb_page_slug', true); ?>
|
||||
<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
|
||||
$slug = (string) ($page_link_slugs[$page->ID] ?? get_post_meta($page->ID, '_kb_page_slug', true));
|
||||
$sourceSlug = (string) get_post_meta($page->ID, '_kb_product_slug', true);
|
||||
$part = (array) ($parts[$sourceSlug] ?? []);
|
||||
?>
|
||||
<li>
|
||||
<a href="<?php echo esc_url($url_builder::page($product->slug, $version->slug, $slug)); ?>">
|
||||
<?php if (! empty($part['label']) && count($parts) > 1) : ?>
|
||||
<span class="kb-page-list__part"><?php echo esc_html((string) $part['label']); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php echo esc_html(get_the_title($page)); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user