feat(admin): Einstellungsseite überarbeitet, auch für das Handy
Verbindung: Probleme vor der Anleitung. Tools: Suche, Filter, Alle ein-/ausklappen, Zurücksetzen je Gruppe. Divi-Reiter in Klartext mit Statussymbolen. Rollenhinweise, Hilfe-Tab, Reiter als Raster und Karten auf dem Handy, Kontrast, Tippflächen, einheitliche Du-Anrede. Bestätigungen über data-lit-mcp-confirm statt Inline-onclick. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
165e61d7fe
commit
96fa58188b
+195
-11
@@ -9,6 +9,11 @@
|
|||||||
gap: 8px 16px;
|
gap: 8px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lit-mcp-logo-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.lit-mcp-logo {
|
.lit-mcp-logo {
|
||||||
display: block;
|
display: block;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -25,6 +30,33 @@
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Phones: the tabs form an even grid instead of ragged wrapped lines, each at least 44 px high
|
||||||
|
* (core's tab is about 34 px). The active tab keeps core's look, marked by its colour and underline.
|
||||||
|
*/
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.lit-mcp .nav-tab-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
|
||||||
|
gap: 4px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp .nav-tab-wrapper .nav-tab {
|
||||||
|
float: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 8px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp .nav-tab-wrapper .nav-tab-active {
|
||||||
|
border-color: #2271b1;
|
||||||
|
box-shadow: inset 0 -2px 0 #2271b1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Muted secondary text (tool names, client URLs). */
|
/* Muted secondary text (tool names, client URLs). */
|
||||||
.lit-mcp-muted,
|
.lit-mcp-muted,
|
||||||
.lit-mcp-tool-name {
|
.lit-mcp-tool-name {
|
||||||
@@ -46,6 +78,19 @@
|
|||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Label and field share a line; the field takes the rest and wraps below on narrow screens. */
|
||||||
|
.lit-mcp-url-line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-url-line .lit-mcp-copy-row {
|
||||||
|
flex: 1 1 22rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.lit-mcp-copy-row {
|
.lit-mcp-copy-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -83,21 +128,59 @@
|
|||||||
color: #50575e;
|
color: #50575e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-status-yes-alt .dashicons,
|
.lit-mcp-state .dashicons {
|
||||||
.lit-mcp-result-yes-alt .dashicons {
|
vertical-align: text-bottom;
|
||||||
color: #00a32a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-status-warning .dashicons,
|
/* Divi tab: the report is a plain list, the icon replaces the bullet. */
|
||||||
.lit-mcp-result-warning .dashicons {
|
.lit-mcp-divi-report {
|
||||||
color: #dba617;
|
list-style: none;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-status-dismiss .dashicons,
|
.lit-mcp-divi-report ul {
|
||||||
.lit-mcp-result-dismiss .dashicons {
|
list-style: disc;
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-status-yes-alt .dashicons {
|
||||||
|
color: #008a20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-status-warning .dashicons {
|
||||||
|
color: #996800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-status-dismiss .dashicons {
|
||||||
color: #d63638;
|
color: #d63638;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Folded help ("Wo finde ich das?") and the Divi detail table. */
|
||||||
|
.lit-mcp-hint,
|
||||||
|
.lit-mcp-divi-details {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-hint summary,
|
||||||
|
.lit-mcp-divi-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-hint p {
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-divi-details summary {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only on phones: text that replaces a hidden column. */
|
||||||
|
.lit-mcp-phone-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tab "Zugriff". */
|
/* Tab "Zugriff". */
|
||||||
.lit-mcp-connections + .description {
|
.lit-mcp-connections + .description {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -113,6 +196,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lit-mcp-phone-only {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
.lit-mcp-connections .column-actions {
|
.lit-mcp-connections .column-actions {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
@@ -122,7 +209,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tab "Tools": legend, collapsible groups (WordPress postboxes) with aligned columns. */
|
/* Tab "Tools": search and open/close above the groups, legend, collapsible groups (WordPress postboxes). */
|
||||||
|
.lit-mcp-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 12px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-tool-search {
|
||||||
|
min-width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-filter-status {
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-groups [hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-group-reset {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phones: touch targets of at least 24 px (WCAG 2.5.8) for the reset link and the folded help. */
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.lit-mcp-group-reset {
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-hint summary,
|
||||||
|
.lit-mcp-divi-details summary {
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Explains the icons that replace the "Art" words on phones. */
|
||||||
|
.lit-mcp-kind-legend {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.lit-mcp-legend {
|
.lit-mcp-legend {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content 1fr;
|
grid-template-columns: max-content 1fr;
|
||||||
@@ -144,7 +272,13 @@
|
|||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The title keeps room of its own (12rem): when title, arrow and controls do not fit one line, the controls
|
||||||
|
* drop to the next line instead of squeezing the title into a narrow column or sending the arrow alone below it.
|
||||||
|
*/
|
||||||
.lit-mcp-groups .postbox .hndle {
|
.lit-mcp-groups .postbox .hndle {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
min-width: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -254,12 +388,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 782px) {
|
@media screen and (max-width: 782px) {
|
||||||
.lit-mcp-group-controls {
|
/* No "order": title, toggle, controls read the same for the eye and for Tab. */
|
||||||
order: 3;
|
.lit-mcp-groups .lit-mcp-group-controls {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lit-mcp-kind-legend {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* Room for "Standard (indirekt)" in the select on phones. */
|
/* Room for "Standard (indirekt)" in the select on phones. */
|
||||||
.lit-mcp-tools-table .column-tool {
|
.lit-mcp-tools-table .column-tool {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
@@ -297,6 +435,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Divi tab: on phones each row of the feature table becomes a card named by data-label. */
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.lit-mcp-divi-features thead {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-divi-features tr,
|
||||||
|
.lit-mcp-divi-features td {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One card per row: a rule and room between them, the feature name first and bold. */
|
||||||
|
.lit-mcp-divi-features tr {
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-divi-features td:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-divi-features code {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-divi-features td::before {
|
||||||
|
content: attr(data-label) ": ";
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lit-mcp-divi-features ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Tab "Protokoll": filters in their own block above the table (core hides ".tablenav .actions"
|
/* Tab "Protokoll": filters in their own block above the table (core hides ".tablenav .actions"
|
||||||
on phones); a core-style list table whose change is the wide primary column. */
|
on phones); a core-style list table whose change is the wide primary column. */
|
||||||
.lit-mcp-log-filters {
|
.lit-mcp-log-filters {
|
||||||
@@ -311,6 +488,13 @@
|
|||||||
max-width: 14rem;
|
max-width: 14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lit-mcp-dates {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.lit-mcp-date {
|
.lit-mcp-date {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+206
-2
@@ -1,5 +1,5 @@
|
|||||||
/* Einstellungen → LIT MCP: copy the server URL, live group feedback and collapsible tool
|
/* Einstellungen → LIT MCP: copy the server URL, live group feedback, search and reset for the
|
||||||
groups, filter paging, and a question before a role save that locks people out. */
|
collapsible tool groups, filter paging, and questions before a destructive or locking-out save. */
|
||||||
( function () {
|
( function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -119,6 +119,206 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setGroupOpen( group, open ) {
|
||||||
|
var toggle = group.querySelector( '.handlediv' );
|
||||||
|
|
||||||
|
group.classList.toggle( 'closed', ! open );
|
||||||
|
if ( toggle ) {
|
||||||
|
toggle.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGroupState() {
|
||||||
|
if ( window.postboxes && window.pagenow ) {
|
||||||
|
window.postboxes.save_state( window.pagenow );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAllUserSet( groups ) {
|
||||||
|
groups.forEach( function ( group ) {
|
||||||
|
group.dataset.litUserSet = '1';
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open or close every group; with `save`, remember it like a click on a single toggle would.
|
||||||
|
function setAllGroups( groups, open, save ) {
|
||||||
|
groups.forEach( function ( group ) {
|
||||||
|
setGroupOpen( group, open );
|
||||||
|
} );
|
||||||
|
if ( save ) {
|
||||||
|
saveGroupState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools: search by name, "only changed", open/close all. Rows and empty groups hide instead of vanish.
|
||||||
|
function setUpToolFilter() {
|
||||||
|
var groups = document.querySelector( '.lit-mcp-groups' );
|
||||||
|
var search = document.querySelector( '.lit-mcp-tool-search' );
|
||||||
|
var only = document.querySelector( '.lit-mcp-only-changed' );
|
||||||
|
var status = document.querySelector( '.lit-mcp-filter-status' );
|
||||||
|
var collapse = document.querySelector( '.lit-mcp-collapse-all' );
|
||||||
|
var expand = document.querySelector( '.lit-mcp-expand-all' );
|
||||||
|
var closedBefore = null;
|
||||||
|
var all;
|
||||||
|
|
||||||
|
if ( ! groups || ! search || ! only || ! collapse || ! expand ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
all = groups.querySelectorAll( '.lit-mcp-group' );
|
||||||
|
|
||||||
|
function countText( shown ) {
|
||||||
|
var text = window.litMcpAdmin;
|
||||||
|
|
||||||
|
if ( ! text ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return shown === 1 ? text.toolShown : text.toolsShown.replace( '%d', shown );
|
||||||
|
}
|
||||||
|
|
||||||
|
// The filter is cleared: forget the temporary state and, if the user changed groups meanwhile, save it now.
|
||||||
|
function finishFilter() {
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
closedBefore = null;
|
||||||
|
all.forEach( function ( group ) {
|
||||||
|
changed = changed || !! group.dataset.litUserSet;
|
||||||
|
delete group.dataset.litUserSet;
|
||||||
|
} );
|
||||||
|
if ( changed ) {
|
||||||
|
saveGroupState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFiltering() {
|
||||||
|
return search.value.trim() !== '' || only.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
var term = search.value.trim().toLowerCase();
|
||||||
|
var filtering = isFiltering();
|
||||||
|
var shown = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A filter opens the groups with hits; clearing it puts every group back as the user had it.
|
||||||
|
* A group the user opens or closes themselves while filtering keeps that choice.
|
||||||
|
*/
|
||||||
|
if ( filtering && closedBefore === null ) {
|
||||||
|
closedBefore = Array.prototype.map.call( all, function ( group ) {
|
||||||
|
return group.classList.contains( 'closed' );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
all.forEach( function ( group, index ) {
|
||||||
|
var visible = 0;
|
||||||
|
|
||||||
|
group.querySelectorAll( 'tbody tr' ).forEach( function ( row ) {
|
||||||
|
var select = row.querySelector( '.lit-mcp-tool-mode' );
|
||||||
|
var cell = row.querySelector( '.column-tool' );
|
||||||
|
var name = cell ? cell.textContent.toLowerCase() : '';
|
||||||
|
var match = ( term === '' || name.indexOf( term ) !== -1 ) &&
|
||||||
|
( ! only.checked || ( select && select.value !== '' ) );
|
||||||
|
|
||||||
|
row.hidden = ! match;
|
||||||
|
visible += match ? 1 : 0;
|
||||||
|
} );
|
||||||
|
group.hidden = visible === 0;
|
||||||
|
if ( filtering && visible > 0 && ! group.dataset.litUserSet ) {
|
||||||
|
setGroupOpen( group, true );
|
||||||
|
} else if ( ! filtering && closedBefore && ! group.dataset.litUserSet ) {
|
||||||
|
setGroupOpen( group, ! closedBefore[ index ] );
|
||||||
|
}
|
||||||
|
shown += visible;
|
||||||
|
} );
|
||||||
|
if ( ! filtering && closedBefore ) {
|
||||||
|
finishFilter();
|
||||||
|
}
|
||||||
|
if ( status ) {
|
||||||
|
status.textContent = filtering ? countText( shown ) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter in the search field would submit the whole settings form, half-done changes included.
|
||||||
|
search.addEventListener( 'keydown', function ( event ) {
|
||||||
|
if ( event.key === 'Enter' ) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
search.addEventListener( 'input', apply );
|
||||||
|
only.addEventListener( 'change', apply );
|
||||||
|
// While filtering, nothing is saved: the hidden groups would be stored as hidden boxes.
|
||||||
|
// "User set" only means something while a filter is on; outside of it a stale flag would stop the next filter.
|
||||||
|
collapse.addEventListener( 'click', function () {
|
||||||
|
if ( isFiltering() ) {
|
||||||
|
markAllUserSet( all );
|
||||||
|
}
|
||||||
|
setAllGroups( all, false, ! isFiltering() );
|
||||||
|
} );
|
||||||
|
expand.addEventListener( 'click', function () {
|
||||||
|
if ( isFiltering() ) {
|
||||||
|
markAllUserSet( all );
|
||||||
|
}
|
||||||
|
setAllGroups( all, true, ! isFiltering() );
|
||||||
|
} );
|
||||||
|
groups.addEventListener( 'click', function ( event ) {
|
||||||
|
var group = event.target.closest( '.handlediv, .hndle' ) && event.target.closest( '.lit-mcp-group' );
|
||||||
|
|
||||||
|
if ( group && isFiltering() ) {
|
||||||
|
group.dataset.litUserSet = '1';
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
// The browser may restore the search text after a reload; make the list match it.
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools: "Auf Standard zurücksetzen" clears a group's mode, its "nur lesen" switch and every tool's own setting.
|
||||||
|
function setUpGroupReset() {
|
||||||
|
document.querySelectorAll( '.lit-mcp-group' ).forEach( function ( group ) {
|
||||||
|
var button = group.querySelector( '.lit-mcp-group-reset' );
|
||||||
|
|
||||||
|
if ( ! button ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.addEventListener( 'click', function () {
|
||||||
|
var readonly = group.querySelector( '.lit-mcp-group-readonly' );
|
||||||
|
var mode = group.querySelector( '.lit-mcp-group-mode' );
|
||||||
|
|
||||||
|
// Order matters: unlock the rows first, then fall back to the default.
|
||||||
|
change( readonly, function () {
|
||||||
|
readonly.checked = false;
|
||||||
|
} );
|
||||||
|
change( mode, function () {
|
||||||
|
mode.value = '';
|
||||||
|
} );
|
||||||
|
group.querySelectorAll( '.lit-mcp-tool-mode' ).forEach( function ( select ) {
|
||||||
|
change( select, function () {
|
||||||
|
select.value = '';
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
if ( window.wp && window.wp.a11y && window.litMcpAdmin ) {
|
||||||
|
window.wp.a11y.speak( window.litMcpAdmin.groupReset );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
function change( field, apply ) {
|
||||||
|
if ( ! field ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apply();
|
||||||
|
field.dispatchEvent( new Event( 'change', { bubbles: true } ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// A button with data-lit-mcp-confirm asks first (revoke a connection, clear the log).
|
||||||
|
function setUpConfirmButtons() {
|
||||||
|
document.addEventListener( 'click', function ( event ) {
|
||||||
|
var button = event.target.closest( '[data-lit-mcp-confirm]' );
|
||||||
|
|
||||||
|
if ( button && ! window.confirm( button.dataset.litMcpConfirm ) ) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
// Protokoll: a changed filter starts again on page 1.
|
// Protokoll: a changed filter starts again on page 1.
|
||||||
function setUpFilterPaging() {
|
function setUpFilterPaging() {
|
||||||
document.querySelectorAll( '.lit-mcp-log-filters select, .lit-mcp-log-filters input' ).forEach(
|
document.querySelectorAll( '.lit-mcp-log-filters select, .lit-mcp-log-filters input' ).forEach(
|
||||||
@@ -162,9 +362,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener( 'DOMContentLoaded', function () {
|
document.addEventListener( 'DOMContentLoaded', function () {
|
||||||
|
// First: the confirmations guard destructive buttons and must not depend on the rest starting up.
|
||||||
|
setUpConfirmButtons();
|
||||||
setUpCopyButton();
|
setUpCopyButton();
|
||||||
setUpGroupToggles();
|
setUpGroupToggles();
|
||||||
setUpLiveGroupFeedback();
|
setUpLiveGroupFeedback();
|
||||||
|
setUpToolFilter();
|
||||||
|
setUpGroupReset();
|
||||||
setUpFilterPaging();
|
setUpFilterPaging();
|
||||||
setUpAccessConfirmation();
|
setUpAccessConfirmation();
|
||||||
} );
|
} );
|
||||||
|
|||||||
+51
-16
@@ -84,7 +84,7 @@ final class AccessTab implements AdminTab
|
|||||||
private static function ownRoleWarning(): string
|
private static function ownRoleWarning(): string
|
||||||
{
|
{
|
||||||
return __(
|
return __(
|
||||||
'Keine Ihrer Rollen ist mehr freigegeben – Sie selbst können LIT MCP nicht mehr nutzen.',
|
'Keine deiner Rollen ist mehr freigegeben. Du kannst LIT MCP dann selbst nicht mehr nutzen.',
|
||||||
'wp-lit-mcp'
|
'wp-lit-mcp'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,11 +105,14 @@ final class AccessTab implements AdminTab
|
|||||||
);
|
);
|
||||||
foreach (wp_roles()->get_names() as $role => $name) {
|
foreach (wp_roles()->get_names() as $role => $name) {
|
||||||
printf(
|
printf(
|
||||||
'<label><input type="checkbox" name="%s[]" value="%s"%s> %s</label><br>',
|
'<label><input type="checkbox" name="%1$s[]" value="%2$s"%3$s'
|
||||||
|
. ' aria-describedby="lit-mcp-role-%2$s"> %4$s</label>'
|
||||||
|
. ' <span class="description" id="lit-mcp-role-%2$s">(%5$s)</span><br>',
|
||||||
esc_attr(Access::OPTION),
|
esc_attr(Access::OPTION),
|
||||||
esc_attr((string) $role),
|
esc_attr((string) $role),
|
||||||
checked(in_array($role, $allowed, true), true, false),
|
checked(in_array($role, $allowed, true), true, false),
|
||||||
esc_html(translate_user_role((string) $name))
|
esc_html(translate_user_role((string) $name)),
|
||||||
|
esc_html(self::roleHint((string) $role))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
echo '</fieldset><p class="description">'
|
echo '</fieldset><p class="description">'
|
||||||
@@ -122,6 +125,19 @@ final class AccessTab implements AdminTab
|
|||||||
$this->renderConnections();
|
$this->renderConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What a role may do in WordPress, in one phrase, so the admin sees whom they are letting in. */
|
||||||
|
private static function roleHint(string $role): string
|
||||||
|
{
|
||||||
|
$caps = (array) (((array) wp_roles()->roles)[$role]['capabilities'] ?? []);
|
||||||
|
|
||||||
|
return match (true) {
|
||||||
|
! empty($caps['manage_options']) => __('verwaltet die Website', 'wp-lit-mcp'),
|
||||||
|
! empty($caps['edit_others_posts']) => __('bearbeitet auch fremde Inhalte', 'wp-lit-mcp'),
|
||||||
|
! empty($caps['edit_posts']) || ! empty($caps['edit_pages']) => __('bearbeitet Inhalte', 'wp-lit-mcp'),
|
||||||
|
default => __('keine Bearbeitungsrechte erkannt', 'wp-lit-mcp'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public function handleRevoke(): void
|
public function handleRevoke(): void
|
||||||
{
|
{
|
||||||
$this->action->guard();
|
$this->action->guard();
|
||||||
@@ -240,17 +256,22 @@ final class AccessTab implements AdminTab
|
|||||||
|
|
||||||
echo '<tr>';
|
echo '<tr>';
|
||||||
echo '<td class="column-user">' . esc_html($connection['user_login']) . '</td>';
|
echo '<td class="column-user">' . esc_html($connection['user_login']) . '</td>';
|
||||||
|
$created = (string) wp_date($dateFormat . ' ' . $timeFormat, $connection['created']);
|
||||||
|
// The "Verbunden am" column is hidden on phones; the date then shows under the client instead.
|
||||||
printf(
|
printf(
|
||||||
'<td class="column-client"><strong>%s</strong><br><span class="lit-mcp-muted">%s</span></td>',
|
'<td class="column-client"><strong>%s</strong><br><span class="lit-mcp-muted">%s</span>'
|
||||||
|
. '<br class="lit-mcp-phone-only"><span class="lit-mcp-muted lit-mcp-phone-only">%s</span></td>',
|
||||||
esc_html($connection['name']),
|
esc_html($connection['name']),
|
||||||
esc_html($connection['app_id'])
|
esc_html($connection['app_id']),
|
||||||
);
|
esc_html(sprintf(
|
||||||
printf(
|
/* translators: %s: date and time */
|
||||||
'<td class="column-created">%s</td>',
|
__('Verbunden am %s', 'wp-lit-mcp'),
|
||||||
esc_html((string) wp_date($dateFormat . ' ' . $timeFormat, $connection['created']))
|
$created
|
||||||
|
))
|
||||||
);
|
);
|
||||||
|
printf('<td class="column-created">%s</td>', esc_html($created));
|
||||||
$lastUsed = $connection['last_used'] === null
|
$lastUsed = $connection['last_used'] === null
|
||||||
? '–'
|
? __('Noch nie', 'wp-lit-mcp')
|
||||||
: (string) wp_date($dateFormat, $connection['last_used']);
|
: (string) wp_date($dateFormat, $connection['last_used']);
|
||||||
echo '<td class="column-last-used">' . esc_html($lastUsed) . '</td>';
|
echo '<td class="column-last-used">' . esc_html($lastUsed) . '</td>';
|
||||||
echo '<td class="column-actions">';
|
echo '<td class="column-actions">';
|
||||||
@@ -259,17 +280,31 @@ final class AccessTab implements AdminTab
|
|||||||
printf('<input type="hidden" name="user_id" value="%s">', esc_attr((string) $connection['user_id']));
|
printf('<input type="hidden" name="user_id" value="%s">', esc_attr((string) $connection['user_id']));
|
||||||
printf('<input type="hidden" name="uuid" value="%s">', esc_attr($connection['uuid']));
|
printf('<input type="hidden" name="uuid" value="%s">', esc_attr($connection['uuid']));
|
||||||
wp_nonce_field(self::REVOKE_ACTION . '_' . $connection['uuid']);
|
wp_nonce_field(self::REVOKE_ACTION . '_' . $connection['uuid']);
|
||||||
$confirm = esc_js(sprintf(
|
|
||||||
/* translators: %s: client name */
|
|
||||||
__('Verbindung „%s“ widerrufen? Der Assistent muss sich danach neu anmelden.', 'wp-lit-mcp'),
|
|
||||||
$connection['name']
|
|
||||||
));
|
|
||||||
submit_button(
|
submit_button(
|
||||||
__('Widerrufen', 'wp-lit-mcp'),
|
__('Widerrufen', 'wp-lit-mcp'),
|
||||||
'delete small',
|
'delete small',
|
||||||
'submit',
|
'submit',
|
||||||
false,
|
false,
|
||||||
['onclick' => "return window.confirm('" . $confirm . "');"]
|
[
|
||||||
|
// One id per row: submit_button() would give every button the id "submit".
|
||||||
|
'id' => 'lit-mcp-revoke-' . $connection['uuid'],
|
||||||
|
// Starts with the visible word (WCAG 2.5.3) and says whose connection it is.
|
||||||
|
'aria-label' => sprintf(
|
||||||
|
/* translators: 1: client name, 2: user login */
|
||||||
|
__('Widerrufen: %1$s, %2$s', 'wp-lit-mcp'),
|
||||||
|
$connection['name'],
|
||||||
|
$connection['user_login']
|
||||||
|
),
|
||||||
|
'data-lit-mcp-confirm' => sprintf(
|
||||||
|
/* translators: 1: client name, 2: user login */
|
||||||
|
__(
|
||||||
|
'Verbindung „%1$s“ von %2$s widerrufen? Der Assistent muss sich danach neu anmelden.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$connection['name'],
|
||||||
|
$connection['user_login']
|
||||||
|
),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
echo '</form></td></tr>';
|
echo '</form></td></tr>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,15 +34,19 @@ final class ConnectionTab implements AdminTab
|
|||||||
|
|
||||||
public function render(): void
|
public function render(): void
|
||||||
{
|
{
|
||||||
|
$checks = $this->status->checks(true);
|
||||||
|
|
||||||
|
$this->renderProblems($checks);
|
||||||
echo '<h2>' . esc_html__('So verbindest du einen KI-Assistenten', 'wp-lit-mcp') . '</h2>';
|
echo '<h2>' . esc_html__('So verbindest du einen KI-Assistenten', 'wp-lit-mcp') . '</h2>';
|
||||||
echo '<ol class="lit-mcp-steps"><li>';
|
echo '<ol class="lit-mcp-steps"><li>';
|
||||||
printf(
|
printf(
|
||||||
'<label for="lit-mcp-url">%s</label>'
|
'<span class="lit-mcp-url-line"><label for="lit-mcp-url">%s</label>'
|
||||||
. '<span class="lit-mcp-copy-row copy-to-clipboard-container">'
|
. '<span class="lit-mcp-copy-row copy-to-clipboard-container">'
|
||||||
. '<input id="lit-mcp-url" type="text" class="large-text code" readonly value="%s">'
|
. '<input id="lit-mcp-url" type="text" class="large-text code" readonly value="%s">'
|
||||||
. '<button type="button" class="button lit-mcp-copy" data-clipboard-target="#lit-mcp-url">%s</button>'
|
. '<button type="button" class="button lit-mcp-copy hide-if-no-js"'
|
||||||
. '<span class="success hidden" aria-hidden="true">%s</span></span>',
|
. ' data-clipboard-target="#lit-mcp-url">%s</button>'
|
||||||
esc_html__('Server-URL kopieren:', 'wp-lit-mcp'),
|
. '<span class="success hidden" aria-hidden="true">%s</span></span></span>',
|
||||||
|
esc_html__('Server-URL', 'wp-lit-mcp'),
|
||||||
esc_attr(ConnectionStatus::serverUrl()),
|
esc_attr(ConnectionStatus::serverUrl()),
|
||||||
esc_html__('URL kopieren', 'wp-lit-mcp'),
|
esc_html__('URL kopieren', 'wp-lit-mcp'),
|
||||||
esc_html__('Kopiert!', 'wp-lit-mcp')
|
esc_html__('Kopiert!', 'wp-lit-mcp')
|
||||||
@@ -50,7 +54,17 @@ final class ConnectionTab implements AdminTab
|
|||||||
echo '</li><li>' . esc_html__(
|
echo '</li><li>' . esc_html__(
|
||||||
'Im KI-Assistenten einen eigenen MCP-Server (Connector) hinzufügen und die URL einfügen.',
|
'Im KI-Assistenten einen eigenen MCP-Server (Connector) hinzufügen und die URL einfügen.',
|
||||||
'wp-lit-mcp'
|
'wp-lit-mcp'
|
||||||
) . '</li><li>';
|
);
|
||||||
|
printf(
|
||||||
|
'<details class="lit-mcp-hint"><summary>%s</summary><p>%s</p></details>',
|
||||||
|
esc_html__('Wo finde ich das?', 'wp-lit-mcp'),
|
||||||
|
esc_html__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
|
'Je nach Assistent heißt der Punkt „Konnektoren“, „Connectors“ oder „MCP-Server“ und steht meist in den Einstellungen. Als Name passt der Name dieser Website; ein Passwort oder einen Schlüssel brauchst du nicht, die Anmeldung läuft über WordPress.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
echo '</li><li>';
|
||||||
printf(
|
printf(
|
||||||
/* translators: %s: link to the "Zugriff" tab */
|
/* translators: %s: link to the "Zugriff" tab */
|
||||||
esc_html__('Anmelden und den Zugriff bestätigen. Danach erscheint die Verbindung unter %s.', 'wp-lit-mcp'),
|
esc_html__('Anmelden und den Zugriff bestätigen. Danach erscheint die Verbindung unter %s.', 'wp-lit-mcp'),
|
||||||
@@ -59,7 +73,7 @@ final class ConnectionTab implements AdminTab
|
|||||||
echo '</li></ol>';
|
echo '</li></ol>';
|
||||||
|
|
||||||
echo '<h2>' . esc_html__('Status', 'wp-lit-mcp') . '</h2><ul class="lit-mcp-status">';
|
echo '<h2>' . esc_html__('Status', 'wp-lit-mcp') . '</h2><ul class="lit-mcp-status">';
|
||||||
foreach ($this->status->checks(true) as $check) {
|
foreach ($checks as $check) {
|
||||||
[$icon, $state] = match ($check['ok']) {
|
[$icon, $state] = match ($check['ok']) {
|
||||||
true => ['yes-alt', __('In Ordnung:', 'wp-lit-mcp')],
|
true => ['yes-alt', __('In Ordnung:', 'wp-lit-mcp')],
|
||||||
false => ['dismiss', __('Problem:', 'wp-lit-mcp')],
|
false => ['dismiss', __('Problem:', 'wp-lit-mcp')],
|
||||||
@@ -83,4 +97,24 @@ final class ConnectionTab implements AdminTab
|
|||||||
esc_html__('Werkzeuge → Website-Zustand', 'wp-lit-mcp')
|
esc_html__('Werkzeuge → Website-Zustand', 'wp-lit-mcp')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A failed check makes the steps above useless, so it shows before them, not only in the list below.
|
||||||
|
*
|
||||||
|
* @param list<array{label: string, ok: ?bool, message: string}> $checks
|
||||||
|
*/
|
||||||
|
private function renderProblems(array $checks): void
|
||||||
|
{
|
||||||
|
$failed = array_filter($checks, static fn (array $check): bool => $check['ok'] === false);
|
||||||
|
if ($failed === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="notice notice-error inline"><p><strong>'
|
||||||
|
. esc_html__('Ein Assistent kann sich noch nicht verbinden.', 'wp-lit-mcp') . '</strong></p><ul>';
|
||||||
|
foreach ($failed as $check) {
|
||||||
|
printf('<li>%s: %s</li>', esc_html($check['label']), esc_html($check['message']));
|
||||||
|
}
|
||||||
|
echo '</ul></div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -58,7 +58,16 @@ final class LimitsTab implements AdminTab
|
|||||||
'log_retention_days' => [
|
'log_retention_days' => [
|
||||||
__('Protokoll aufbewahren', 'wp-lit-mcp'),
|
__('Protokoll aufbewahren', 'wp-lit-mcp'),
|
||||||
__('Tage', 'wp-lit-mcp'),
|
__('Tage', 'wp-lit-mcp'),
|
||||||
__('Ältere Einträge im Protokoll werden automatisch gelöscht.', 'wp-lit-mcp'),
|
sprintf(
|
||||||
|
/* translators: 1: minimum days, 2: maximum days */
|
||||||
|
__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
||||||
|
'Zwischen %1$d und %2$d Tagen. Ältere Einträge im Protokoll werden automatisch gelöscht.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$bounds['log_retention_days'][0],
|
||||||
|
$bounds['log_retention_days'][1]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ final class LogTab implements AdminTab
|
|||||||
}
|
}
|
||||||
echo '</select>';
|
echo '</select>';
|
||||||
|
|
||||||
|
printf('<span class="lit-mcp-dates" role="group" aria-label="%s">', esc_attr__('Zeitraum', 'wp-lit-mcp'));
|
||||||
foreach (['date_from' => __('von', 'wp-lit-mcp'), 'date_to' => __('bis', 'wp-lit-mcp')] as $name => $label) {
|
foreach (['date_from' => __('von', 'wp-lit-mcp'), 'date_to' => __('bis', 'wp-lit-mcp')] as $name => $label) {
|
||||||
$id = 'lit-mcp-filter-' . str_replace('_', '-', $name);
|
$id = 'lit-mcp-filter-' . str_replace('_', '-', $name);
|
||||||
printf(
|
printf(
|
||||||
@@ -267,6 +268,7 @@ final class LogTab implements AdminTab
|
|||||||
esc_attr($extra[$name])
|
esc_attr($extra[$name])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
echo '</span>';
|
||||||
|
|
||||||
submit_button(__('Filtern', 'wp-lit-mcp'), '', 'filter_action', false, ['id' => 'lit-mcp-filter-submit']);
|
submit_button(__('Filtern', 'wp-lit-mcp'), '', 'filter_action', false, ['id' => 'lit-mcp-filter-submit']);
|
||||||
|
|
||||||
@@ -319,10 +321,6 @@ final class LogTab implements AdminTab
|
|||||||
printf('<div class="lit-mcp-clear-log"><form method="post" action="%s">', esc_url(admin_url('admin-post.php')));
|
printf('<div class="lit-mcp-clear-log"><form method="post" action="%s">', esc_url(admin_url('admin-post.php')));
|
||||||
printf('<input type="hidden" name="action" value="%s">', esc_attr(self::CLEAR_ACTION));
|
printf('<input type="hidden" name="action" value="%s">', esc_attr(self::CLEAR_ACTION));
|
||||||
wp_nonce_field(self::CLEAR_ACTION);
|
wp_nonce_field(self::CLEAR_ACTION);
|
||||||
$confirmMessage = esc_js(__(
|
|
||||||
'Das gesamte Protokoll unwiderruflich löschen?',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
));
|
|
||||||
submit_button(
|
submit_button(
|
||||||
sprintf(
|
sprintf(
|
||||||
/* translators: %d: number of log entries */
|
/* translators: %d: number of log entries */
|
||||||
@@ -332,7 +330,7 @@ final class LogTab implements AdminTab
|
|||||||
'delete',
|
'delete',
|
||||||
'submit',
|
'submit',
|
||||||
false,
|
false,
|
||||||
['onclick' => "return window.confirm('" . $confirmMessage . "');"]
|
['data-lit-mcp-confirm' => __('Das gesamte Protokoll unwiderruflich löschen?', 'wp-lit-mcp')]
|
||||||
);
|
);
|
||||||
echo '</form></div>';
|
echo '</form></div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ final class SettingsPage
|
|||||||
add_action('admin_menu', [$this, 'addMenu']);
|
add_action('admin_menu', [$this, 'addMenu']);
|
||||||
add_action('admin_init', [$this, 'registerSettings']);
|
add_action('admin_init', [$this, 'registerSettings']);
|
||||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||||
|
add_filter('removable_query_args', [$this, 'removableQueryArgs']);
|
||||||
foreach ($this->tabs() as $tab) {
|
foreach ($this->tabs() as $tab) {
|
||||||
$tab->registerActions();
|
$tab->registerActions();
|
||||||
}
|
}
|
||||||
@@ -51,13 +52,72 @@ final class SettingsPage
|
|||||||
|
|
||||||
public function addMenu(): void
|
public function addMenu(): void
|
||||||
{
|
{
|
||||||
add_options_page(
|
$hook = add_options_page(
|
||||||
__('LIT MCP', 'wp-lit-mcp'),
|
__('LIT MCP', 'wp-lit-mcp'),
|
||||||
__('LIT MCP', 'wp-lit-mcp'),
|
__('LIT MCP', 'wp-lit-mcp'),
|
||||||
'manage_options',
|
'manage_options',
|
||||||
self::SLUG,
|
self::SLUG,
|
||||||
[$this, 'render']
|
[$this, 'render']
|
||||||
);
|
);
|
||||||
|
if (is_string($hook)) {
|
||||||
|
add_action('load-' . $hook, [$this, 'addHelpTab']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WordPress removes these from the address bar after the page loaded, so a reload or a copied link
|
||||||
|
* does not show a finished action's notice again.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $args
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function removableQueryArgs(array $args): array
|
||||||
|
{
|
||||||
|
$args[] = 'lit_mcp_notice';
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Short "Hilfe" panel at the top right; the tabs explain themselves, this only says where to start. */
|
||||||
|
public function addHelpTab(): void
|
||||||
|
{
|
||||||
|
$screen = get_current_screen();
|
||||||
|
if ($screen !== null) {
|
||||||
|
$this->helpTabFor($screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param object $screen A WP_Screen. */
|
||||||
|
public function helpTabFor(object $screen): void
|
||||||
|
{
|
||||||
|
$items = [
|
||||||
|
__('Verbinden', 'wp-lit-mcp') => __(
|
||||||
|
'Tab „Verbindung“: Server-URL kopieren und im KI-Assistenten als MCP-Server hinzufügen.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
__('Wer darf?', 'wp-lit-mcp') => __(
|
||||||
|
'Tab „Zugriff“: Rollen festlegen und bestehende Verbindungen widerrufen.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
__('Was darf der Assistent?', 'wp-lit-mcp') => __(
|
||||||
|
'Tab „Tools“: jedes Tool direkt, indirekt oder aus; ganze Gruppen auf „nur lesen“ stellen.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
__('Was ist passiert?', 'wp-lit-mcp') => __(
|
||||||
|
'Tab „Protokoll“: jede Änderung des Assistenten mit Benutzer, Ergebnis und Zeitpunkt.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
$content = '';
|
||||||
|
foreach ($items as $title => $text) {
|
||||||
|
$content .= sprintf('<p><strong>%s</strong> %s</p>', esc_html($title), esc_html($text));
|
||||||
|
}
|
||||||
|
|
||||||
|
$screen->add_help_tab([
|
||||||
|
'id' => 'lit-mcp-overview',
|
||||||
|
'title' => __('Überblick', 'wp-lit-mcp'),
|
||||||
|
'content' => $content,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerSettings(): void
|
public function registerSettings(): void
|
||||||
@@ -87,6 +147,10 @@ final class SettingsPage
|
|||||||
wp_enqueue_script('lit-mcp-admin', $base . '/js/admin.js', $deps, $this->version, true);
|
wp_enqueue_script('lit-mcp-admin', $base . '/js/admin.js', $deps, $this->version, true);
|
||||||
wp_localize_script('lit-mcp-admin', 'litMcpAdmin', [
|
wp_localize_script('lit-mcp-admin', 'litMcpAdmin', [
|
||||||
'copied' => __('Die URL wurde in die Zwischenablage kopiert.', 'wp-lit-mcp'),
|
'copied' => __('Die URL wurde in die Zwischenablage kopiert.', 'wp-lit-mcp'),
|
||||||
|
/* translators: %d: number of tools matching the filter */
|
||||||
|
'toolsShown' => __('%d Tools angezeigt', 'wp-lit-mcp'),
|
||||||
|
'toolShown' => __('1 Tool angezeigt', 'wp-lit-mcp'),
|
||||||
|
'groupReset' => __('Gruppe auf Standard zurückgesetzt. Noch nicht gespeichert.', 'wp-lit-mcp'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +264,7 @@ final class SettingsPage
|
|||||||
],
|
],
|
||||||
'divi_withdrawn' => [
|
'divi_withdrawn' => [
|
||||||
'success',
|
'success',
|
||||||
__('Freigabe zurückgezogen; schreibende Divi-Werkzeuge sind wieder gesperrt.', 'wp-lit-mcp'),
|
__('Freigabe zurückgezogen; schreibende Divi-Tools sind wieder gesperrt.', 'wp-lit-mcp'),
|
||||||
],
|
],
|
||||||
'divi_withdraw_failed' => [
|
'divi_withdraw_failed' => [
|
||||||
'error',
|
'error',
|
||||||
|
|||||||
+72
-12
@@ -87,6 +87,7 @@ final class ToolsTab implements AdminTab
|
|||||||
echo '<form method="post" action="options.php">';
|
echo '<form method="post" action="options.php">';
|
||||||
settings_fields(self::GROUP);
|
settings_fields(self::GROUP);
|
||||||
submit_button(null, 'primary', 'submit-top');
|
submit_button(null, 'primary', 'submit-top');
|
||||||
|
$this->renderToolbar();
|
||||||
// The admin script mirrors a group's mode onto its rows before saving; it needs the label pattern.
|
// The admin script mirrors a group's mode onto its rows before saving; it needs the label pattern.
|
||||||
printf(
|
printf(
|
||||||
'<div class="lit-mcp-groups" data-like-group="%s">',
|
'<div class="lit-mcp-groups" data-like-group="%s">',
|
||||||
@@ -105,10 +106,11 @@ final class ToolsTab implements AdminTab
|
|||||||
private function renderIntro(): void
|
private function renderIntro(): void
|
||||||
{
|
{
|
||||||
echo '<p class="description">'
|
echo '<p class="description">'
|
||||||
. esc_html__('Lege fest, welche Tools der KI-Assistent nutzen kann.', 'wp-lit-mcp') . ' '
|
. esc_html__('Lege fest, welche Tools der KI-Assistent nutzen kann.', 'wp-lit-mcp') . '</p>';
|
||||||
. esc_html__('Änderungen gelten ab der nächsten Verbindung.', 'wp-lit-mcp') . ' '
|
echo '<div class="notice notice-info inline"><p><strong>'
|
||||||
. esc_html__('Im KI-Assistenten die Verbindung dafür gegebenenfalls neu herstellen.', 'wp-lit-mcp')
|
. esc_html__('Änderungen gelten ab der nächsten Verbindung.', 'wp-lit-mcp') . '</strong> '
|
||||||
. '</p>';
|
. esc_html__('Stelle die Verbindung im KI-Assistenten danach neu her.', 'wp-lit-mcp')
|
||||||
|
. '</p></div>';
|
||||||
|
|
||||||
echo '<dl class="lit-mcp-legend">';
|
echo '<dl class="lit-mcp-legend">';
|
||||||
$legend = [
|
$legend = [
|
||||||
@@ -123,6 +125,26 @@ final class ToolsTab implements AdminTab
|
|||||||
echo '</dl>';
|
echo '</dl>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Search, "only changed" and open/close for the many tools; needs the admin script, so it hides without JS. */
|
||||||
|
private function renderToolbar(): void
|
||||||
|
{
|
||||||
|
printf(
|
||||||
|
'<div class="lit-mcp-toolbar hide-if-no-js">'
|
||||||
|
. '<label class="screen-reader-text" for="lit-mcp-tool-search">%1$s</label>'
|
||||||
|
. '<input type="search" id="lit-mcp-tool-search" class="lit-mcp-tool-search" placeholder="%1$s">'
|
||||||
|
. '<label><input type="checkbox" class="lit-mcp-only-changed"> %2$s</label>'
|
||||||
|
. '<button type="button" class="button lit-mcp-collapse-all">%3$s</button>'
|
||||||
|
. '<button type="button" class="button lit-mcp-expand-all">%4$s</button>'
|
||||||
|
. '<span class="lit-mcp-filter-status" role="status"></span></div>',
|
||||||
|
esc_attr__('Tool suchen', 'wp-lit-mcp'),
|
||||||
|
esc_html__('Nur Tools mit eigener Einstellung', 'wp-lit-mcp'),
|
||||||
|
esc_html__('Alle einklappen', 'wp-lit-mcp'),
|
||||||
|
esc_html__('Alle aufklappen', 'wp-lit-mcp')
|
||||||
|
);
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in kindLegend().
|
||||||
|
echo '<p class="description lit-mcp-kind-legend">' . $this->kindLegend() . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
/** Only when the direct tools get many: then the count is worth a warning, otherwise it is noise. */
|
/** Only when the direct tools get many: then the count is worth a warning, otherwise it is noise. */
|
||||||
private function renderCounter(int $direct): void
|
private function renderCounter(int $direct): void
|
||||||
{
|
{
|
||||||
@@ -196,7 +218,7 @@ final class ToolsTab implements AdminTab
|
|||||||
)),
|
)),
|
||||||
esc_html($this->summary($modes, $policy->isReadonly($group)))
|
esc_html($this->summary($modes, $policy->isReadonly($group)))
|
||||||
);
|
);
|
||||||
$this->renderGroupControls($policy, $group, $mixed);
|
// Title, toggle, controls: the same order for the eye and for Tab, so the stylesheet needs no "order".
|
||||||
printf(
|
printf(
|
||||||
'<div class="handle-actions hide-if-no-js"><button type="button" class="handlediv" aria-expanded="%s">'
|
'<div class="handle-actions hide-if-no-js"><button type="button" class="handlediv" aria-expanded="%s">'
|
||||||
. '<span class="screen-reader-text">%s</span>'
|
. '<span class="screen-reader-text">%s</span>'
|
||||||
@@ -208,6 +230,7 @@ final class ToolsTab implements AdminTab
|
|||||||
$label
|
$label
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
$this->renderGroupControls($policy, $group, $label, $mixed);
|
||||||
echo '</div><div class="inside">';
|
echo '</div><div class="inside">';
|
||||||
|
|
||||||
printf(
|
printf(
|
||||||
@@ -239,7 +262,7 @@ final class ToolsTab implements AdminTab
|
|||||||
return $readonly ? __('nur lesen', 'wp-lit-mcp') . ' · ' . $summary : $summary;
|
return $readonly ? __('nur lesen', 'wp-lit-mcp') . ' · ' . $summary : $summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderGroupControls(ExposurePolicy $policy, string $group, bool $mixed): void
|
private function renderGroupControls(ExposurePolicy $policy, string $group, string $label, bool $mixed): void
|
||||||
{
|
{
|
||||||
$selectId = 'lit-mcp-group-mode-' . self::idPart($group);
|
$selectId = 'lit-mcp-group-mode-' . self::idPart($group);
|
||||||
$groupMode = $policy->groupMode($group);
|
$groupMode = $policy->groupMode($group);
|
||||||
@@ -275,6 +298,15 @@ final class ToolsTab implements AdminTab
|
|||||||
checked($policy->isReadonly($group), true, false),
|
checked($policy->isReadonly($group), true, false),
|
||||||
esc_html__('Nur lesen', 'wp-lit-mcp')
|
esc_html__('Nur lesen', 'wp-lit-mcp')
|
||||||
);
|
);
|
||||||
|
printf(
|
||||||
|
'<button type="button" class="button-link lit-mcp-group-reset hide-if-no-js" aria-label="%s">%s</button>',
|
||||||
|
esc_attr(sprintf(
|
||||||
|
/* translators: %s: group label */
|
||||||
|
__('Auf Standard zurücksetzen: %s', 'wp-lit-mcp'),
|
||||||
|
$label
|
||||||
|
)),
|
||||||
|
esc_html__('Auf Standard zurücksetzen', 'wp-lit-mcp')
|
||||||
|
);
|
||||||
if ($mixed) {
|
if ($mixed) {
|
||||||
echo '<span class="description">'
|
echo '<span class="description">'
|
||||||
. esc_html__('gemischt – einzelne Tools haben eigene Einstellungen', 'wp-lit-mcp')
|
. esc_html__('gemischt – einzelne Tools haben eigene Einstellungen', 'wp-lit-mcp')
|
||||||
@@ -389,13 +421,12 @@ final class ToolsTab implements AdminTab
|
|||||||
private function kind(Ability $ability): string
|
private function kind(Ability $ability): string
|
||||||
{
|
{
|
||||||
$annotations = $ability->annotations();
|
$annotations = $ability->annotations();
|
||||||
|
$styles = self::kindStyles();
|
||||||
[$icon, $text] = match (true) {
|
[$icon, $text] = match (true) {
|
||||||
isset(self::DELETING[$ability->name()]) => ['trash', self::DELETING[$ability->name()] === 'trash'
|
isset(self::DELETING[$ability->name()]) => $styles[self::DELETING[$ability->name()]],
|
||||||
? __('Papierkorb', 'wp-lit-mcp')
|
($annotations['destructive'] ?? false) === true => $styles['overwrite'],
|
||||||
: __('löscht', 'wp-lit-mcp')],
|
($annotations['readonly'] ?? false) === true => $styles['read'],
|
||||||
($annotations['destructive'] ?? false) === true => ['update', __('überschreibt', 'wp-lit-mcp')],
|
default => $styles['write'],
|
||||||
($annotations['readonly'] ?? false) === true => ['visibility', __('liest', 'wp-lit-mcp')],
|
|
||||||
default => ['edit', __('schreibt', 'wp-lit-mcp')],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
@@ -406,6 +437,35 @@ final class ToolsTab implements AdminTab
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{string, string}> Kind → [dashicon, word]. */
|
||||||
|
private static function kindStyles(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'read' => ['visibility', __('liest', 'wp-lit-mcp')],
|
||||||
|
'write' => ['edit', __('schreibt', 'wp-lit-mcp')],
|
||||||
|
'overwrite' => ['update', __('überschreibt', 'wp-lit-mcp')],
|
||||||
|
'delete' => ['trash', __('löscht', 'wp-lit-mcp')],
|
||||||
|
'trash' => ['trash', __('Papierkorb', 'wp-lit-mcp')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** On phones the "Art" column shows icons only; this line says what they mean (touch has no tooltips). */
|
||||||
|
private function kindLegend(): string
|
||||||
|
{
|
||||||
|
$styles = self::kindStyles();
|
||||||
|
$parts = [];
|
||||||
|
foreach (['read', 'write', 'overwrite', 'delete'] as $key) {
|
||||||
|
[$icon, $text] = $styles[$key];
|
||||||
|
$parts[] = sprintf(
|
||||||
|
'<span class="dashicons dashicons-%s" aria-hidden="true"></span> %s',
|
||||||
|
esc_attr($icon),
|
||||||
|
esc_html($key === 'delete' ? $text . ' / ' . $styles['trash'][1] : $text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' · ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
/** Safe fragment for an HTML id from a group id or tool name ("lit/guide" → "lit-guide"). */
|
/** Safe fragment for an HTML id from a group id or tool name ("lit/guide" → "lit-guide"). */
|
||||||
private static function idPart(string $value): string
|
private static function idPart(string $value): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ final class DiviTab implements AdminTab
|
|||||||
/* translators: 1: Divi version, 2: comma separated feature names */
|
/* translators: 1: Divi version, 2: comma separated feature names */
|
||||||
__(
|
__(
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
'LIT MCP: Mit Divi %1$s sind diese Fähigkeiten nicht nutzbar: %2$s. Die betroffenen Werkzeuge lehnen ab, alle anderen laufen weiter.',
|
'LIT MCP: Mit Divi %1$s sind diese Fähigkeiten nicht nutzbar: %2$s. Die betroffenen Tools lehnen ab, alle anderen laufen weiter.',
|
||||||
'wp-lit-mcp'
|
'wp-lit-mcp'
|
||||||
),
|
),
|
||||||
$this->version(),
|
$this->version(),
|
||||||
@@ -211,7 +211,7 @@ final class DiviTab implements AdminTab
|
|||||||
echo '<h2>' . esc_html__('Divi-Freigabe', 'wp-lit-mcp') . '</h2>';
|
echo '<h2>' . esc_html__('Divi-Freigabe', 'wp-lit-mcp') . '</h2>';
|
||||||
echo '<p>' . esc_html__(
|
echo '<p>' . esc_html__(
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
'LIT MCP prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Werkzeuge ab, die sie brauchen; alle anderen laufen weiter.',
|
'LIT MCP prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Tools ab, die sie brauchen; alle anderen laufen weiter.',
|
||||||
'wp-lit-mcp'
|
'wp-lit-mcp'
|
||||||
) . '</p>';
|
) . '</p>';
|
||||||
if ($this->removed !== []) {
|
if ($this->removed !== []) {
|
||||||
@@ -225,6 +225,7 @@ final class DiviTab implements AdminTab
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->renderNextStep($status);
|
||||||
$this->renderStatus($status);
|
$this->renderStatus($status);
|
||||||
$this->renderFeatures($status);
|
$this->renderFeatures($status);
|
||||||
$this->renderForms($status, $report);
|
$this->renderForms($status, $report);
|
||||||
@@ -233,6 +234,45 @@ final class DiviTab implements AdminTab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One sentence on what to do now, above the details; a released version needs none. */
|
||||||
|
private function renderNextStep(ReleaseStatus $status): void
|
||||||
|
{
|
||||||
|
if ($status->version === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = match ($status->state) {
|
||||||
|
ReleaseState::Untested => sprintf(
|
||||||
|
/* translators: %s: Divi version */
|
||||||
|
__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
||||||
|
'Divi %s ist noch nicht geprüft. Starte unten die Prüfung; danach kannst du die Version freigeben.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$status->version
|
||||||
|
),
|
||||||
|
ReleaseState::Incompatible => sprintf(
|
||||||
|
/* translators: %s: Divi version */
|
||||||
|
__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
||||||
|
'Divi %s hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Tools bleiben gesperrt, bis ein Plugin-Update erscheint.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$status->version
|
||||||
|
),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
if ($text === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-%s inline"><p><strong>%s</strong></p></div>',
|
||||||
|
$status->state === ReleaseState::Incompatible ? 'error' : 'warning',
|
||||||
|
esc_html($text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function renderStatus(ReleaseStatus $status): void
|
private function renderStatus(ReleaseStatus $status): void
|
||||||
{
|
{
|
||||||
echo '<table class="form-table" role="presentation"><tbody>';
|
echo '<table class="form-table" role="presentation"><tbody>';
|
||||||
@@ -242,9 +282,10 @@ final class DiviTab implements AdminTab
|
|||||||
esc_html($status->version === '' ? __('nicht erkannt', 'wp-lit-mcp') : $status->version)
|
esc_html($status->version === '' ? __('nicht erkannt', 'wp-lit-mcp') : $status->version)
|
||||||
);
|
);
|
||||||
printf(
|
printf(
|
||||||
'<tr><th scope="row">%s</th><td><strong>%s</strong> – %s</td></tr>',
|
'<tr><th scope="row">%s</th><td>%s – %s</td></tr>',
|
||||||
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
||||||
esc_html($this->stateLabel($status->state)),
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
||||||
|
$this->stateBadge($status->state),
|
||||||
esc_html($this->explanation($status))
|
esc_html($this->explanation($status))
|
||||||
);
|
);
|
||||||
if ($status->problems !== []) {
|
if ($status->problems !== []) {
|
||||||
@@ -259,31 +300,47 @@ final class DiviTab implements AdminTab
|
|||||||
|
|
||||||
private function renderFeatures(ReleaseStatus $status): void
|
private function renderFeatures(ReleaseStatus $status): void
|
||||||
{
|
{
|
||||||
echo '<h3>' . esc_html__('Fähigkeiten', 'wp-lit-mcp') . '</h3>';
|
// Open when something needs attention; a released version keeps the long table folded.
|
||||||
|
printf(
|
||||||
|
'<details class="lit-mcp-divi-details"%s><summary>%s</summary>',
|
||||||
|
$status->state === ReleaseState::Released ? '' : ' open',
|
||||||
|
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
|
||||||
|
);
|
||||||
echo '<table class="widefat striped lit-mcp-divi-features"><thead><tr>';
|
echo '<table class="widefat striped lit-mcp-divi-features"><thead><tr>';
|
||||||
$headings = [
|
$headings = [
|
||||||
__('Fähigkeit', 'wp-lit-mcp'),
|
__('Fähigkeit', 'wp-lit-mcp'),
|
||||||
__('Zustand', 'wp-lit-mcp'),
|
__('Zustand', 'wp-lit-mcp'),
|
||||||
__('Quelle', 'wp-lit-mcp'),
|
__('Quelle', 'wp-lit-mcp'),
|
||||||
__('Probleme', 'wp-lit-mcp'),
|
__('Probleme', 'wp-lit-mcp'),
|
||||||
__('Betroffene Werkzeuge', 'wp-lit-mcp'),
|
__('Betroffene Tools', 'wp-lit-mcp'),
|
||||||
];
|
];
|
||||||
foreach ($headings as $heading) {
|
foreach ($headings as $heading) {
|
||||||
echo '<th scope="col">' . esc_html($heading) . '</th>';
|
echo '<th scope="col">' . esc_html($heading) . '</th>';
|
||||||
}
|
}
|
||||||
echo '</tr></thead><tbody>';
|
echo '</tr></thead><tbody>';
|
||||||
foreach (($this->overview ?? FeatureOverview::withoutTools())->rows($status->matrix) as $row) {
|
foreach (($this->overview ?? FeatureOverview::withoutTools())->rows($status->matrix) as $row) {
|
||||||
echo '<tr><td>' . esc_html($row['label']) . '</td>';
|
// data-label: on phones each row becomes a card and the cell names itself.
|
||||||
echo '<td><strong>' . esc_html($this->stateLabel(ReleaseState::from($row['state']))) . '</strong></td>';
|
printf('<tr><td data-label="%s">%s</td>', esc_attr($headings[0]), esc_html($row['label']));
|
||||||
echo '<td>' . esc_html($this->sourceLabel($row['source'])) . '</td>';
|
printf(
|
||||||
echo '<td>';
|
'<td data-label="%s">%s</td>',
|
||||||
|
esc_attr($headings[1]),
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
||||||
|
$this->stateBadge(ReleaseState::from($row['state']))
|
||||||
|
);
|
||||||
|
printf(
|
||||||
|
'<td data-label="%s">%s</td>',
|
||||||
|
esc_attr($headings[2]),
|
||||||
|
esc_html($this->sourceLabel($row['source']))
|
||||||
|
);
|
||||||
|
printf('<td data-label="%s">', esc_attr($headings[3]));
|
||||||
$this->renderList($row['problems'], false);
|
$this->renderList($row['problems'], false);
|
||||||
echo '</td><td>';
|
printf('</td><td data-label="%s">', esc_attr($headings[4]));
|
||||||
$this->renderList($row['tools'], true);
|
$this->renderList($row['tools'], true);
|
||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->renderAutomationNote();
|
$this->renderAutomationNote();
|
||||||
|
echo '</details>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<string> $items */
|
/** @param list<string> $items */
|
||||||
@@ -305,9 +362,9 @@ final class DiviTab implements AdminTab
|
|||||||
private function sourceLabel(?string $source): string
|
private function sourceLabel(?string $source): string
|
||||||
{
|
{
|
||||||
return match ($source) {
|
return match ($source) {
|
||||||
ReleaseStatus::SOURCE_CODE => __('Plugin (code)', 'wp-lit-mcp'),
|
ReleaseStatus::SOURCE_CODE => __('Im Plugin als getestet hinterlegt', 'wp-lit-mcp'),
|
||||||
ReleaseStatus::SOURCE_DATABASE => __('Freigabe auf dieser Website (database)', 'wp-lit-mcp'),
|
ReleaseStatus::SOURCE_DATABASE => __('Von dir auf dieser Website freigegeben', 'wp-lit-mcp'),
|
||||||
ReleaseStatus::SOURCE_AUTO => __('Automatik (auto)', 'wp-lit-mcp'),
|
ReleaseStatus::SOURCE_AUTO => __('Automatisch geprüft', 'wp-lit-mcp'),
|
||||||
default => '–',
|
default => '–',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -331,6 +388,23 @@ final class DiviTab implements AdminTab
|
|||||||
) . '</p>';
|
) . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Same coloured icons as the status list on the tab "Verbindung"; the word stays, colour is never alone. */
|
||||||
|
private function stateBadge(ReleaseState $state): string
|
||||||
|
{
|
||||||
|
$icon = match ($state) {
|
||||||
|
ReleaseState::Released => 'yes-alt',
|
||||||
|
ReleaseState::Untested => 'warning',
|
||||||
|
ReleaseState::Incompatible => 'dismiss',
|
||||||
|
};
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<span class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
|
||||||
|
. ' aria-hidden="true"></span> <strong>%2$s</strong></span>',
|
||||||
|
$icon,
|
||||||
|
esc_html($this->stateLabel($state))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function stateLabel(ReleaseState $state): string
|
private function stateLabel(ReleaseState $state): string
|
||||||
{
|
{
|
||||||
return match ($state) {
|
return match ($state) {
|
||||||
@@ -352,7 +426,7 @@ final class DiviTab implements AdminTab
|
|||||||
if ($status->state === ReleaseState::Untested) {
|
if ($status->state === ReleaseState::Untested) {
|
||||||
return __(
|
return __(
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
'Diese Version ist nicht geprüft. Die Werkzeuge der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
|
'Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
|
||||||
'wp-lit-mcp'
|
'wp-lit-mcp'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -446,8 +520,18 @@ final class DiviTab implements AdminTab
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
foreach ($report->steps as $step) {
|
foreach ($report->steps as $step) {
|
||||||
$mark = $step->skipped ? '–' : ($step->green() ? '✓' : '✗');
|
[$icon, $word] = match (true) {
|
||||||
printf('<li>%s %s', esc_html($mark), esc_html($step->label()));
|
$step->skipped => ['minus', __('übersprungen', 'wp-lit-mcp')],
|
||||||
|
$step->green() => ['yes-alt', __('bestanden', 'wp-lit-mcp')],
|
||||||
|
default => ['dismiss', __('fehlgeschlagen', 'wp-lit-mcp')],
|
||||||
|
};
|
||||||
|
printf(
|
||||||
|
'<li class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
|
||||||
|
. ' aria-hidden="true"></span><span class="screen-reader-text">%2$s:</span> %3$s',
|
||||||
|
esc_attr($icon),
|
||||||
|
esc_html($word),
|
||||||
|
esc_html($step->label())
|
||||||
|
);
|
||||||
if ($step->skipped) {
|
if ($step->skipped) {
|
||||||
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
|
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ final class AccessTabTest extends TestCase
|
|||||||
Functions\when('settings_fields')->justReturn(null);
|
Functions\when('settings_fields')->justReturn(null);
|
||||||
Functions\when('submit_button')->alias(static function ($text = null, $type = 'primary', $name = 'submit', $wrap = true, $other = ''): void {
|
Functions\when('submit_button')->alias(static function ($text = null, $type = 'primary', $name = 'submit', $wrap = true, $other = ''): void {
|
||||||
$attrs = is_array($other) ? implode(' ', array_map(
|
$attrs = is_array($other) ? implode(' ', array_map(
|
||||||
static fn ($k, $v): string => sprintf('%s="%s"', $k, $v),
|
static fn ($k, $v): string => sprintf('%s="%s"', $k, htmlspecialchars((string) $v, ENT_QUOTES)),
|
||||||
array_keys($other),
|
array_keys($other),
|
||||||
$other
|
$other
|
||||||
)) : '';
|
)) : '';
|
||||||
@@ -238,7 +238,14 @@ final class AccessTabTest extends TestCase
|
|||||||
self::assertStringContainsString('name="user_id" value="3"', $html);
|
self::assertStringContainsString('name="user_id" value="3"', $html);
|
||||||
self::assertStringContainsString('value="lit_mcp_revoke"', $html);
|
self::assertStringContainsString('value="lit_mcp_revoke"', $html);
|
||||||
self::assertStringContainsString('nonce-lit_mcp_revoke_uuid-1', $html);
|
self::assertStringContainsString('nonce-lit_mcp_revoke_uuid-1', $html);
|
||||||
self::assertStringContainsString('confirm(', $html);
|
self::assertStringContainsString(
|
||||||
|
'data-lit-mcp-confirm="Verbindung „Claude <script>“ von redaktion<b> widerrufen?',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('aria-label="Widerrufen: Claude <script>, redaktion<b>"', $html);
|
||||||
|
self::assertStringContainsString('id="lit-mcp-revoke-uuid-1"', $html);
|
||||||
|
self::assertStringContainsString('aria-describedby="lit-mcp-role-editor"', $html);
|
||||||
|
self::assertStringNotContainsString('onclick', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The admin script asks before a save that leaves nobody, or the admin themselves, with access. */
|
/** The admin script asks before a save that leaves nobody, or the admin themselves, with access. */
|
||||||
@@ -251,7 +258,7 @@ final class AccessTabTest extends TestCase
|
|||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'<form method="post" action="options.php" class="lit-mcp-access-form"'
|
'<form method="post" action="options.php" class="lit-mcp-access-form"'
|
||||||
. ' data-confirm-none="Keine Rolle freigegeben – niemand kann LIT MCP nutzen. Trotzdem speichern?"'
|
. ' data-confirm-none="Keine Rolle freigegeben – niemand kann LIT MCP nutzen. Trotzdem speichern?"'
|
||||||
. ' data-confirm-own="Keine Ihrer Rollen ist mehr freigegeben – Sie selbst können LIT MCP nicht mehr nutzen.'
|
. ' data-confirm-own="Keine deiner Rollen ist mehr freigegeben. Du kannst LIT MCP dann selbst nicht mehr nutzen.'
|
||||||
. ' Trotzdem speichern?"'
|
. ' Trotzdem speichern?"'
|
||||||
. ' data-own-roles="editor">',
|
. ' data-own-roles="editor">',
|
||||||
$html
|
$html
|
||||||
@@ -293,7 +300,7 @@ final class AccessTabTest extends TestCase
|
|||||||
|
|
||||||
self::assertStringContainsString('d.m.Y H:i@1790000000', $html);
|
self::assertStringContainsString('d.m.Y H:i@1790000000', $html);
|
||||||
self::assertStringContainsString('d.m.Y@1790050000', $html);
|
self::assertStringContainsString('d.m.Y@1790050000', $html);
|
||||||
self::assertStringContainsString('<td class="column-last-used">–</td>', $html);
|
self::assertStringContainsString('<td class="column-last-used">Noch nie</td>', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRenderWithoutConnectionsShowsAnEmptyStateRowLinkingToTheConnectionTab(): void
|
public function testRenderWithoutConnectionsShowsAnEmptyStateRowLinkingToTheConnectionTab(): void
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ final class ConnectionTabTest extends TestCase
|
|||||||
);
|
);
|
||||||
self::assertStringContainsString('<label for="lit-mcp-url"', $html);
|
self::assertStringContainsString('<label for="lit-mcp-url"', $html);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'<button type="button" class="button lit-mcp-copy" data-clipboard-target="#lit-mcp-url">URL kopieren</button>',
|
'<button type="button" class="button lit-mcp-copy hide-if-no-js" data-clipboard-target="#lit-mcp-url">URL kopieren</button>',
|
||||||
$html
|
$html
|
||||||
);
|
);
|
||||||
self::assertStringContainsString('<span class="success hidden" aria-hidden="true">Kopiert!</span>', $html);
|
self::assertStringContainsString('<span class="success hidden" aria-hidden="true">Kopiert!</span>', $html);
|
||||||
@@ -74,4 +74,32 @@ final class ConnectionTabTest extends TestCase
|
|||||||
self::assertStringContainsString('<span class="screen-reader-text">Problem:</span>', $html);
|
self::assertStringContainsString('<span class="screen-reader-text">Problem:</span>', $html);
|
||||||
self::assertStringContainsString('href="https://kunde.de/wp-admin/site-health.php"', $html);
|
self::assertStringContainsString('href="https://kunde.de/wp-admin/site-health.php"', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAFailedCheckShowsAboveTheStepsNotOnlyInTheList(): void
|
||||||
|
{
|
||||||
|
$html = $this->render([
|
||||||
|
['label' => 'HTTPS', 'ok' => false, 'message' => 'Die Website läuft ohne HTTPS.'],
|
||||||
|
['label' => 'Discovery', 'ok' => null, 'message' => 'noch nicht geprüft'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertStringContainsString('<div class="notice notice-error inline">', $html);
|
||||||
|
self::assertStringContainsString('<li>HTTPS: Die Website läuft ohne HTTPS.</li>', $html);
|
||||||
|
self::assertStringNotContainsString('<li>Discovery:', $html);
|
||||||
|
self::assertLessThan(strpos($html, '<ol class="lit-mcp-steps">'), strpos($html, 'notice-error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoProblemNoticeWhenNothingFailed(): void
|
||||||
|
{
|
||||||
|
$html = $this->render([['label' => 'HTTPS', 'ok' => true, 'message' => 'aktiv']]);
|
||||||
|
|
||||||
|
self::assertStringNotContainsString('notice-error', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExplainsWhereTheAssistantMenuIsWithoutNamingAProduct(): void
|
||||||
|
{
|
||||||
|
$html = $this->render([]);
|
||||||
|
|
||||||
|
self::assertStringContainsString('<details class="lit-mcp-hint"><summary>Wo finde ich das?</summary>', $html);
|
||||||
|
self::assertStringContainsString('<label for="lit-mcp-url">Server-URL</label>', $html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ final class LimitsTabTest extends TestCase
|
|||||||
self::assertStringContainsString('Höchstens 32', $html);
|
self::assertStringContainsString('Höchstens 32', $html);
|
||||||
self::assertStringContainsString('10 MB', $html);
|
self::assertStringContainsString('10 MB', $html);
|
||||||
self::assertStringContainsString('min="1" max="3650"', $html);
|
self::assertStringContainsString('min="1" max="3650"', $html);
|
||||||
|
self::assertStringContainsString('Zwischen 1 und 3650 Tagen.', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWarnsWhenUploadMbExceedsTheServersOwnUploadLimit(): void
|
public function testWarnsWhenUploadMbExceedsTheServersOwnUploadLimit(): void
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ final class LogTabTest extends TestCase
|
|||||||
|
|
||||||
self::assertStringContainsString('<div class="lit-mcp-clear-log">', $two);
|
self::assertStringContainsString('<div class="lit-mcp-clear-log">', $two);
|
||||||
self::assertStringContainsString('lit_mcp_clear_log', $two);
|
self::assertStringContainsString('lit_mcp_clear_log', $two);
|
||||||
self::assertStringContainsString('confirm(', $two);
|
self::assertStringContainsString('data-lit-mcp-confirm="Das gesamte Protokoll unwiderruflich löschen?"', $two);
|
||||||
self::assertStringContainsString('nonce-lit_mcp_clear_log', $two);
|
self::assertStringContainsString('nonce-lit_mcp_clear_log', $two);
|
||||||
self::assertStringContainsString('>Protokoll leeren (2 Einträge)</button>', $two);
|
self::assertStringContainsString('>Protokoll leeren (2 Einträge)</button>', $two);
|
||||||
self::assertStringContainsString('>Protokoll leeren (1 Eintrag)</button>', $this->render([], 1));
|
self::assertStringContainsString('>Protokoll leeren (1 Eintrag)</button>', $this->render([], 1));
|
||||||
|
|||||||
@@ -204,4 +204,25 @@ final class SettingsPageTest extends TestCase
|
|||||||
['divi_withdraw_failed', 'error', 'keine Freigabe'],
|
['divi_withdraw_failed', 'error', 'keine Freigabe'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTheNoticeParameterIsRemovedFromTheAddressBarAfterwards(): void
|
||||||
|
{
|
||||||
|
$page = new SettingsPage([$this->tab('verbindung', 'Verbindung')]);
|
||||||
|
|
||||||
|
self::assertSame(['a', 'lit_mcp_notice'], $page->removableQueryArgs(['a']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddsAnOverviewHelpTab(): void
|
||||||
|
{
|
||||||
|
Functions\stubEscapeFunctions();
|
||||||
|
Functions\stubTranslationFunctions();
|
||||||
|
$screen = Mockery::mock();
|
||||||
|
$screen->expects('add_help_tab')->once()->with(Mockery::on(
|
||||||
|
static fn (array $tab): bool => $tab['id'] === 'lit-mcp-overview'
|
||||||
|
&& str_contains($tab['content'], 'Tab „Verbindung“')
|
||||||
|
&& str_contains($tab['content'], 'Tab „Tools“')
|
||||||
|
));
|
||||||
|
|
||||||
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->helpTabFor($screen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,6 +354,39 @@ final class ToolsTabTest extends TestCase
|
|||||||
self::assertStringNotContainsString('Divi-Code-Modul', $withoutDivi);
|
self::assertStringNotContainsString('Divi-Code-Modul', $withoutDivi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReconnectHintIsAVisibleNoticeNotGreyFinePrint(): void
|
||||||
|
{
|
||||||
|
$html = $this->render([]);
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'<div class="notice notice-info inline"><p><strong>Änderungen gelten ab der nächsten Verbindung.</strong>',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOffersSearchOnlyChangedOpenCloseAndAGroupResetForTheScript(): void
|
||||||
|
{
|
||||||
|
$html = $this->render([]);
|
||||||
|
|
||||||
|
self::assertStringContainsString('class="lit-mcp-toolbar hide-if-no-js"', $html);
|
||||||
|
self::assertStringContainsString('<input type="search" id="lit-mcp-tool-search"', $html);
|
||||||
|
self::assertStringContainsString('lit-mcp-only-changed', $html);
|
||||||
|
self::assertStringContainsString('lit-mcp-collapse-all', $html);
|
||||||
|
self::assertStringContainsString('lit-mcp-expand-all', $html);
|
||||||
|
self::assertSame(3, substr_count($html, 'class="button-link lit-mcp-group-reset hide-if-no-js"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPhoneLegendNamesTheIconsOfTheArtColumn(): void
|
||||||
|
{
|
||||||
|
$html = $this->render([]);
|
||||||
|
|
||||||
|
self::assertMatchesRegularExpression(
|
||||||
|
'#lit-mcp-kind-legend">.*dashicons-visibility.*liest.*dashicons-edit.*schreibt.*dashicons-update.*überschreibt'
|
||||||
|
. '.*dashicons-trash.*löscht / Papierkorb#s',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testHasASubmitButtonAboveAndBelowTheGroups(): void
|
public function testHasASubmitButtonAboveAndBelowTheGroups(): void
|
||||||
{
|
{
|
||||||
$html = $this->render([]);
|
$html = $this->render([]);
|
||||||
|
|||||||
@@ -380,14 +380,14 @@ final class DiviTabTest extends TestCase
|
|||||||
|
|
||||||
$html = $this->renderedWith(self::mixedStatus(), null, $overview);
|
$html = $this->renderedWith(self::mixedStatus(), null, $overview);
|
||||||
|
|
||||||
self::assertStringContainsString('<th scope="col">Betroffene Werkzeuge</th>', $html);
|
self::assertStringContainsString('<th scope="col">Betroffene Tools</th>', $html);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'<td>Seiten schreiben</td><td><strong>inkompatibel</strong></td><td>–</td>',
|
'<td data-label="Fähigkeit">Seiten schreiben</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-dismiss"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span> <strong>inkompatibel</strong></span></td><td data-label="Quelle">–</td>',
|
||||||
$html
|
$html
|
||||||
);
|
);
|
||||||
self::assertStringContainsString('<td>Modulkatalog</td><td><strong>ungetestet</strong></td>', $html);
|
self::assertStringContainsString('<td data-label="Fähigkeit">Modulkatalog</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-warning"><span class="dashicons dashicons-warning" aria-hidden="true"></span> <strong>ungetestet</strong></span></td>', $html);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'<td>Seiten lesen</td><td><strong>freigegeben</strong></td><td>Automatik (auto)</td>',
|
'<td data-label="Fähigkeit">Seiten lesen</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-yes-alt"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span> <strong>freigegeben</strong></span></td><td data-label="Quelle">Automatisch geprüft</td>',
|
||||||
$html
|
$html
|
||||||
);
|
);
|
||||||
self::assertStringContainsString('<li><code>lit-divi-insert</code></li>', $html);
|
self::assertStringContainsString('<li><code>lit-divi-insert</code></li>', $html);
|
||||||
@@ -412,8 +412,8 @@ final class DiviTabTest extends TestCase
|
|||||||
self::report()
|
self::report()
|
||||||
);
|
);
|
||||||
|
|
||||||
self::assertStringContainsString('Plugin (code)', $code);
|
self::assertStringContainsString('Im Plugin als getestet hinterlegt', $code);
|
||||||
self::assertStringContainsString('Freigabe auf dieser Website (database)', $database);
|
self::assertStringContainsString('Von dir auf dieser Website freigegeben', $database);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTheTabExplainsTheAutomationAndTheFilter(): void
|
public function testTheTabExplainsTheAutomationAndTheFilter(): void
|
||||||
@@ -538,4 +538,23 @@ final class DiviTabTest extends TestCase
|
|||||||
|
|
||||||
$tab->registerActions();
|
$tab->registerActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testNamesTheNextStepAboveTheDetailsAndKeepsAReleasedTableFolded(): void
|
||||||
|
{
|
||||||
|
$incompatible = $this->rendered(self::mixedStatus(), null);
|
||||||
|
$released = $this->rendered(ReleaseMatrices::released('5.3.0'), null);
|
||||||
|
|
||||||
|
self::assertStringContainsString('notice notice-error inline', $incompatible);
|
||||||
|
self::assertStringContainsString('<details class="lit-mcp-divi-details" open>', $incompatible);
|
||||||
|
self::assertStringNotContainsString('notice notice-', $released);
|
||||||
|
self::assertStringContainsString('<details class="lit-mcp-divi-details">', $released);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUntestedVersionNamesTheCheckAsTheNextStep(): void
|
||||||
|
{
|
||||||
|
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), null);
|
||||||
|
|
||||||
|
self::assertStringContainsString('notice notice-warning inline', $html);
|
||||||
|
self::assertStringContainsString('Divi 5.3.0 ist noch nicht geprüft. Starte unten die Prüfung', $html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user