WordPress-Entwurfsmanagement

Der eine oder die andere mag es schon gemerkt haben, das Bloggen (für die Kinder: “Was ist Bloggen?“) hat bei mir wieder an Stellenwert gewonnen. Konkret heißt das, dass ich wieder bei einer Rate von mehr als einem Post pro Jahr bin. Eine meiner vielen seltsamen Eigenschaften ist wohl auch die, dass ein Projekt für mich erst als abgeschlossen zu gelten scheint, wenn es in irgend einer Form mit der anonymen Masse (haha, schön wär’s!) geteilt wurde. Kleine Dinge kommen nach Mastodon, größere hier ins Blog.

Und so begab es sich, dass in den letzten Wochen und Monaten die Liste der Entwürfe im Backend dieses Blogs zu wachsen begann und für meine Verhältnisse ungeahnte Ausmaße annahm. Es wurde unübersichtlich. Welche Entwürfe sind in welchem Stadium? In welcher Reihenfolge will ich sie publizieren? Da gab es alles zwischen der unkonkreten Idee bis hin zum nahezu fertigen Post. Außerdem gab es Posts, die am besten gestern hätten veröffentlich werden sollen und wiederum andere, bei denen mir Sankt-Nimmerleins-Tag locker noch reichen würde.

Was also dringend ins Werk gesetzt werden wollte (“ins Werk setzen”, eine Formulierung, die meiner Wahrnehmung nach gerade einen Aufwärtstrend erlebt): Ein WordPress-Plugin, welches im Dashboard die Blogpost-Entwürfe auflistet und in der man gleichzeitig den Reifegrad und die Priorität der einzelnen Posts einstellen kann.

Vor Urzeiten hatte ich den Vorgänger eines solchen Plugins geschrieben, damals noch so richtig mit der Hand. Es könnte sogar in vi gewesen sein, aber ich will nicht prahlen.. Jedenfalls war jetzt die Zeit gekommen, das Plugin mit Hilfe der üblichen Helferlein etwas aufzubohren. Und so sieht das jetzt aus:

Und falls jemanden der Code interessieren sollte, bitteschön:

<?php
/*
Plugin Name: [dasaweb] DashboardPipeline
Description: Zeigt geplante Artikel im Dashboard – Entwürfe per Drag & Drop sortieren und Fertigstellungsgrad setzen.
Version:     1.0
Author:      Daniel Weber
Author URI:  https://dasaweb.de/
License:     GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: dasaweb-zeige-pipeline-auf-dashboard
Requires at least: 5.5
Requires PHP: 7.4
Update URI:  false
*/

// Direktaufruf der Datei abweisen - sie läuft nur innerhalb von WordPress
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Rechte, die man braucht, um die Pipeline aller Autoren zu sehen und zu pflegen
const DASAWEB_ZPAD_CAP = 'edit_others_posts';

// user_meta-Schlüssel für die manuell gezogene Reihenfolge der Entwürfe
const DASAWEB_ZPAD_META = 'dasaweb_zpad_reihenfolge';

// post_meta-Schlüssel für den Fertigstellungsgrad (0-100, in 10er-Schritten)
const DASAWEB_ZPAD_FORTSCHRITT = '_dasaweb_zpad_fortschritt';


add_action( 'init', 'dasaweb_DashboardPipeline_meta_registrieren' );
add_action( 'wp_dashboard_setup', 'dasaweb_DashboardPipeline_setup' );
add_action( 'admin_enqueue_scripts', 'dasaweb_DashboardPipeline_assets' );
add_action( 'wp_ajax_dasaweb_zpad_reihenfolge', 'dasaweb_DashboardPipeline_reihenfolge_speichern' );
add_action( 'wp_ajax_dasaweb_zpad_fortschritt', 'dasaweb_DashboardPipeline_fortschritt_speichern' );
add_action( 'transition_post_status', 'dasaweb_DashboardPipeline_fortschritt_aufraeumen', 10, 3 );

// Schreibt nur dann in die Datenbank, wenn der Hook noch nicht eingetragen ist
register_uninstall_hook( __FILE__, 'dasaweb_DashboardPipeline_deinstallieren' );


/**
 * Den Fortschritts-Wert als Post-Meta anmelden.
 *
 * Der führende Unterstrich macht das Feld "protected": es taucht nicht in der
 * Tabelle "Benutzerdefinierte Felder" auf, gepflegt wird es allein über das Widget.
 */
function dasaweb_DashboardPipeline_meta_registrieren(): void {

	register_post_meta( 'post', DASAWEB_ZPAD_FORTSCHRITT, [
		'type'              => 'integer',
		'single'            => true,
		'default'           => 0,
		'show_in_rest'      => false,
		'sanitize_callback' => 'dasaweb_DashboardPipeline_fortschritt_pruefen',
	] );
}


/**
 * Klemmt einen beliebigen Eingabewert auf 0-100 und rastet ihn auf 10er-Schritte.
 */
function dasaweb_DashboardPipeline_fortschritt_pruefen( $wert ): int {

	$zahl = max( 0, min( 100, (int) $wert ) );

	return (int) ( round( $zahl / 10 ) * 10 );
}


/**
 * Balkenfarbe zum Fortschritt: 0% = rot, 50% = gelb, 100% = grün.
 *
 * Achtung: dieselbe Formel steckt im JavaScript weiter unten, damit der Balken
 * schon beim Ziehen des Reglers die Farbe wechselt. Beide zusammen ändern.
 */
function dasaweb_DashboardPipeline_farbe( int $prozent ): string {

	return 'hsl(' . (int) round( $prozent * 1.2 ) . ', 70%, 42%)';
}


/**
 * Liest die gespeicherte Reihenfolge und wendet sie auf die Entwürfe an.
 *
 * Entwürfe, die noch in keiner gespeicherten Reihenfolge vorkommen - also neue -,
 * werden hinten angehängt. Weil die Query nach Datum aufsteigend sortiert, landet
 * der jüngste Entwurf dabei ganz unten.
 *
 * IDs in der gespeicherten Reihenfolge, die es nicht mehr gibt (veröffentlicht,
 * gelöscht), werden übersprungen und verschwinden beim nächsten Speichern von selbst.
 *
 * @param WP_Post[] $posts
 * @return WP_Post[]
 */
function dasaweb_DashboardPipeline_reihenfolge_anwenden( array $posts ): array {

	$reihenfolge = get_user_meta( get_current_user_id(), DASAWEB_ZPAD_META, true );

	if ( ! is_array( $reihenfolge ) || ! $reihenfolge ) {
		return $posts;
	}

	$offen = [];
	foreach ( $posts as $eintrag ) {
		$offen[ $eintrag->ID ] = $eintrag;
	}

	$sortiert = [];
	foreach ( $reihenfolge as $id ) {
		if ( isset( $offen[ $id ] ) ) {
			$sortiert[] = $offen[ $id ];
			unset( $offen[ $id ] );
		}
	}

	return array_merge( $sortiert, array_values( $offen ) );
}


/**
 * Gibt einen Abschnitt des Widgets aus: Überschrift, Liste, Button.
 *
 * Erwartete Schlüssel in $args:
 *   post_status   Status, nach dem gesucht wird ('future' oder 'draft')
 *   orderby       Sortierfeld der WP_Query
 *   order         Sortierrichtung
 *   sortable      true = Liste ist mit der Maus sortierbar, die gespeicherte
 *                 Reihenfolge sticht dann 'orderby'/'order' aus; diese beiden
 *                 entscheiden nur noch, wo neue Einträge hinten einsortiert werden
 *   progress      true = zweite Zeile mit Fortschrittsregler
 *   use_modified  true  = angezeigt wird das Änderungsdatum (post_modified)
 *                 false = angezeigt wird das Erscheinungsdatum (post_date)
 *   heading       Überschrift des Abschnitts
 *   date_prefix   Text vor dem Datum, z.B. "erscheint am"
 *   empty         Meldung, wenn es nichts anzuzeigen gibt
 *   button        Beschriftung des Buttons unter der Liste
 */
function dasaweb_DashboardPipeline_abschnitt( array $args ): void {

	$query = new WP_Query( [
		'post_type'      => 'post',
		'post_status'    => $args['post_status'],
		'orderby'        => $args['orderby'],
		'order'          => $args['order'],
		'posts_per_page' => -1, // Sorgt dafür, dass ALLE angezeigt werden
		'no_found_rows'  => true, // Kein Blättern, also muss auch niemand die Treffer zählen
	] );

	$posts       = $query->posts;
	$sortierbar  = ! empty( $args['sortable'] );
	$fortschritt = ! empty( $args['progress'] );
	$format_lang = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
	$format_kurz = get_option( 'date_format' );

	if ( $sortierbar ) {
		$posts = dasaweb_DashboardPipeline_reihenfolge_anwenden( $posts );
	}

	echo '<div class="dasaweb-zpad-abschnitt">';
	echo '<h3 class="dasaweb-zpad-titel">' . esc_html( $args['heading'] ) . '</h3>';

	if ( ! $posts ) {
		echo '<p class="dasaweb-zpad-leer">' . esc_html( $args['empty'] ) . '</p>';
		echo '</div>';
		return;
	}

	printf(
		'<ul class="dasaweb-zpad-liste%s"%s>',
		$sortierbar ? ' dasaweb-zpad-sortierbar' : '',
		$sortierbar ? ' id="dasaweb-zpad-entwuerfe"' : ''
	);

	foreach ( $posts as $eintrag ) {
		$url   = get_edit_post_link( $eintrag->ID );
		$title = _draft_or_post_title( $eintrag->ID ); // liefert bereits per esc_html() escapten Text

		if ( $args['use_modified'] ) {
			$stamp_lang = get_the_modified_time( $format_lang, $eintrag );
			$stamp_kurz = get_the_modified_time( $format_kurz, $eintrag );
		} else {
			$stamp_lang = get_the_time( $format_lang, $eintrag );
			$stamp_kurz = get_the_time( $format_kurz, $eintrag );
		}

		echo '<li data-id="' . absint( $eintrag->ID ) . '">';

		if ( $sortierbar ) {
			echo '<span class="dasaweb-zpad-griff dashicons dashicons-menu" aria-hidden="true"></span>';
		}

		// $title steckt hier bewusst ohne esc_attr() in den Attributen: _draft_or_post_title()
		// escapt mit ENT_QUOTES, ein zweiter Durchgang würde "Tom & Jerry" zu "Tom &amp;amp; Jerry" machen.
		echo '<a class="dasaweb-zpad-link" href="' . esc_url( $url ) . '" title="'
			. sprintf( __( 'Bearbeiten: &#8222;%s&#8220;', 'dasaweb-zeige-pipeline-auf-dashboard' ), $title )
			. '">' . $title . '</a>';

		echo '<span class="dasaweb-zpad-datum" title="' . esc_attr( $args['date_prefix'] . ' ' . $stamp_lang ) . '">'
			. esc_html( $stamp_kurz ) . '</span>';

		if ( $fortschritt ) {
			$prozent = dasaweb_DashboardPipeline_fortschritt_pruefen(
				get_post_meta( $eintrag->ID, DASAWEB_ZPAD_FORTSCHRITT, true )
			);

			echo '<span class="dasaweb-zpad-fortschritt">';

			// Nativer Range-Slider statt Eigenbau: Klick auf die Leiste, Ziehen am Knopf und
			// Pfeiltasten funktionieren ohne eine Zeile JavaScript. Die Füllung ist ein
			// Farbverlauf mit hartem Stopp, gesteuert über die beiden CSS-Variablen.
			echo '<input type="range" class="dasaweb-zpad-regler"'
				. ' min="0" max="100" step="10"'
				. ' value="' . $prozent . '"'
				. ' data-id="' . absint( $eintrag->ID ) . '"'
				. ' style="--zpad-wert:' . $prozent . '%;--zpad-farbe:'
					. esc_attr( dasaweb_DashboardPipeline_farbe( $prozent ) ) . '"'
				. ' title="' . esc_attr( $prozent . ' %' ) . '"'
				. ' aria-label="'
					. sprintf( __( 'Fertigstellung von &#8222;%s&#8220;', 'dasaweb-zeige-pipeline-auf-dashboard' ), $title )
				. '">';

			echo '</span>';
		}

		echo '</li>';
	}

	echo '</ul>';

	echo '<p class="dasaweb-zpad-fuss"><a class="button" href="'
		. esc_url( admin_url( 'edit.php?post_status=' . $args['post_status'] ) )
		. '">' . esc_html( $args['button'] ) . '</a>';

	if ( $sortierbar || $fortschritt ) {
		echo '<span class="dasaweb-zpad-status" id="dasaweb-zpad-status" aria-live="polite"></span>';
	}

	echo '</p>';
	echo '</div>';
}


/**
 * Inhalt des Dashboard-Widgets: erst die geplanten Artikel, dann die Entwürfe.
 */
function dasaweb_DashboardPipeline(): void {

	// Geplante Artikel - Reihenfolge ergibt sich aus dem Erscheinungstermin,
	// der nächste zuerst. Hier von Hand zu sortieren wäre sinnlos, und fertig
	// sind sie ohnehin, sonst stünde kein Termin dran.
	dasaweb_DashboardPipeline_abschnitt( [
		'post_status'  => 'future',
		'orderby'      => 'date',
		'order'        => 'ASC',
		'sortable'     => false,
		'progress'     => false,
		'use_modified' => false,
		'heading'      => __( 'Geplante Artikel', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'date_prefix'  => __( 'erscheint am', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'empty'        => __( 'Es gibt im Moment keine geplanten Artikel.', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'button'       => __( 'Alle geplanten Artikel zeigen', 'dasaweb-zeige-pipeline-auf-dashboard' ),
	] );

	// Entwürfe - Reihenfolge zieht der Nutzer selbst; 'date'/'ASC' bestimmt nur,
	// wo noch nie einsortierte Entwürfe hinten landen (der jüngste ganz unten).
	dasaweb_DashboardPipeline_abschnitt( [
		'post_status'  => 'draft',
		'orderby'      => 'date',
		'order'        => 'ASC',
		'sortable'     => true,
		'progress'     => true,
		'use_modified' => true,
		'heading'      => __( 'Entwürfe', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'date_prefix'  => __( 'zuletzt bearbeitet am', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'empty'        => __( 'Es gibt im Moment keine Entwürfe.', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'button'       => __( 'Alle Entwürfe zeigen', 'dasaweb-zeige-pipeline-auf-dashboard' ),
	] );
}


/**
 * Dashboard Widget hinzufügen via Funktion wp_add_dashboard_widget()
 */
function dasaweb_DashboardPipeline_setup(): void {

	// wp_add_dashboard_widget() prüft selbst keine Rechte. Ohne diese Abfrage sähe
	// jeder eingeloggte Nutzer - auch Abonnenten - die Titel sämtlicher Entwürfe.
	if ( ! current_user_can( DASAWEB_ZPAD_CAP ) ) {
		return;
	}

	wp_add_dashboard_widget(
		'dasaweb_DashboardPipeline',
		__( 'Geplante Artikel & Entwürfe', 'dasaweb-zeige-pipeline-auf-dashboard' )
			. ' <small>' . __( 'aller Autoren', 'dasaweb-zeige-pipeline-auf-dashboard' ) . '</small>',
		'dasaweb_DashboardPipeline'
	);
}


/**
 * CSS und das Skript für Sortieren und Fortschritt - nur auf dem Dashboard selbst.
 *
 * Beides hängt an Handles ohne eigene Datei (src = false); das ist der WP-Weg,
 * um Inline-Code auszugeben und trotzdem Abhängigkeiten sauber zu deklarieren.
 */
function dasaweb_DashboardPipeline_assets( string $hook ): void {

	if ( 'index.php' !== $hook || ! current_user_can( DASAWEB_ZPAD_CAP ) ) {
		return;
	}

	$css = '
	/* Das Widget rechnet durchgängig mit border-box, unabhängig davon, was das
	   Admin-Theme vorgibt. Ohne diese Zeile addiert sich das padding-left der
	   Fortschrittszeile auf ihre flex-basis von 100% - der Regler steht dann rechts
	   aus der Box heraus, sichtbar aber erst bei 100%, wenn der Überstand grün wird. */
	.dasaweb-zpad-abschnitt,
	.dasaweb-zpad-abschnitt * { box-sizing: border-box; }

	.dasaweb-zpad-abschnitt + .dasaweb-zpad-abschnitt { margin-top: 24px; }
	.dasaweb-zpad-titel {
		margin: 0 0 4px;
		padding-bottom: 6px;
		border-bottom: 1px solid #dcdcde;
		font-size: 12px;
		font-weight: 600;
		text-transform: uppercase;
		letter-spacing: .05em;
		color: #50575e;
	}
	.dasaweb-zpad-leer { color: #787c82; font-style: italic; }
	.dasaweb-zpad-liste { margin: 0; }
	.dasaweb-zpad-liste li {
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		gap: 4px 8px;
		margin: 0;
		padding: 7px 4px;
		border-bottom: 1px solid #f0f0f1;
	}
	.dasaweb-zpad-liste li:last-child { border-bottom: 0; }
	.dasaweb-zpad-link { flex: 1 1 auto; text-decoration: none; }
	.dasaweb-zpad-datum {
		flex: 0 0 auto;
		font-size: 11px;
		color: #8c8f94;
		white-space: nowrap;
	}
	.dasaweb-zpad-griff {
		flex: 0 0 auto;
		align-self: center;
		width: 16px;
		height: 16px;
		font-size: 16px;
		line-height: 1;
		color: #c3c4c7;
		cursor: grab;
	}
	.dasaweb-zpad-sortierbar li:hover { background: #f6f7f7; }
	.dasaweb-zpad-sortierbar li:hover .dasaweb-zpad-griff { color: #787c82; }
	.dasaweb-zpad-griff:active { cursor: grabbing; }
	.dasaweb-zpad-platzhalter {
		border: 1px dashed #c3c4c7;
		background: #fff;
		visibility: visible !important;
	}

	/* zweite Zeile: Fortschrittsregler, per 100%-Basis auf einen eigenen Umbruch gezwungen */
	.dasaweb-zpad-fortschritt {
		flex: 1 0 100%;
		display: flex;
		align-items: center;
		gap: 8px;
		padding-left: 24px;
	}
	.dasaweb-zpad-regler {
		flex: 1 1 auto;
		min-width: 0;
		-webkit-appearance: none;
		appearance: none;
		width: 100%;
		height: 6px;
		margin: 0;
		padding: 0;
		border: 0;
		border-radius: 3px;
		outline: none;
		cursor: pointer;
		background: linear-gradient(
			to right,
			var(--zpad-farbe) 0,
			var(--zpad-farbe) var(--zpad-wert),
			#e6e7e8 var(--zpad-wert),
			#e6e7e8 100%
		);
	}
	/* Die Spur transparent halten - gefärbt wird über den Verlauf am Element selbst. */
	.dasaweb-zpad-regler::-webkit-slider-runnable-track { height: 6px; border: 0; background: transparent; }
	.dasaweb-zpad-regler::-moz-range-track { height: 6px; border: 0; background: transparent; }
	.dasaweb-zpad-regler::-moz-range-progress { height: 6px; background: transparent; }
	.dasaweb-zpad-regler::-webkit-slider-thumb {
		-webkit-appearance: none;
		appearance: none;
		width: 12px;
		height: 12px;
		margin-top: -3px;
		border: 2px solid var(--zpad-farbe);
		border-radius: 50%;
		background: #fff;
		box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
		cursor: grab;
	}
	.dasaweb-zpad-regler::-moz-range-thumb {
		width: 12px;
		height: 12px;
		border: 2px solid var(--zpad-farbe);
		border-radius: 50%;
		background: #fff;
		box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
		cursor: grab;
	}
	.dasaweb-zpad-regler:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px #2271b1; }
	.dasaweb-zpad-regler:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 2px #2271b1; }

	.dasaweb-zpad-fuss { margin-bottom: 0; }
	.dasaweb-zpad-status { margin-left: 8px; font-size: 11px; color: #787c82; }
	.dasaweb-zpad-status.fehler { color: #d63638; }
	';

	wp_register_style( 'dasaweb-zpad', false );
	wp_enqueue_style( 'dasaweb-zpad' );
	wp_add_inline_style( 'dasaweb-zpad', $css );

	$js = '
	jQuery( function ( $ ) {
		var $liste = $( "#dasaweb-zpad-entwuerfe" );

		if ( ! $liste.length ) {
			return;
		}

		var $status = $( "#dasaweb-zpad-status" );
		var timer;

		// Jede neue Meldung stoppt den Timer der vorigen - sonst räumt etwa der Timer
		// von "speichere" eine gerade erst erschienene Fehlermeldung wieder weg.
		function melden( text, istFehler, ausblenden ) {
			clearTimeout( timer );
			$status.toggleClass( "fehler", !! istFehler ).text( text );

			if ( ausblenden ) {
				timer = setTimeout( function () { $status.text( "" ); }, 2000 );
			}
		}

		function senden( daten ) {
			melden( dasawebZpad.speichert );

			daten.nonce = dasawebZpad.nonce;

			$.post( ajaxurl, daten ).done( function ( antwort ) {
				if ( antwort && antwort.success ) {
					melden( dasawebZpad.gespeichert, false, true );
				} else {
					melden( dasawebZpad.fehler, true );
				}
			} ).fail( function () {
				melden( dasawebZpad.fehler, true );
			} );
		}

		// Während des Ziehens nur färben - dieselbe Formel wie in PHP.
		// "input" feuert bei jedem Zehnerschritt, hier darf also kein Request raus.
		$liste.on( "input", ".dasaweb-zpad-regler", function () {
			var regler = this;
			var wert   = parseInt( regler.value, 10 ) || 0;

			regler.style.setProperty( "--zpad-wert", wert + "%" );
			regler.style.setProperty( "--zpad-farbe", "hsl(" + Math.round( wert * 1.2 ) + ", 70%, 42%)" );

			regler.title = wert + " %";
		} );

		// Gespeichert wird erst beim Loslassen bzw. nach einer Tasteneingabe
		$liste.on( "change", ".dasaweb-zpad-regler", function () {
			senden( {
				action: "dasaweb_zpad_fortschritt",
				post: $( this ).data( "id" ),
				wert: parseInt( this.value, 10 ) || 0
			} );
		} );

		if ( ! $.fn.sortable ) {
			return;
		}

		$liste.sortable( {
			items: "> li",
			handle: ".dasaweb-zpad-griff",
			axis: "y",
			cursor: "grabbing",
			opacity: 0.7,
			placeholder: "dasaweb-zpad-platzhalter",
			forcePlaceholderSize: true,
			update: function () {
				senden( {
					action: "dasaweb_zpad_reihenfolge",
					order: $liste.children( "li" ).map( function () {
						return $( this ).data( "id" );
					} ).get()
				} );
			}
		} );
	} );
	';

	wp_register_script( 'dasaweb-zpad', false, [ 'jquery', 'jquery-ui-sortable' ], false, true );
	wp_enqueue_script( 'dasaweb-zpad' );
	wp_localize_script( 'dasaweb-zpad', 'dasawebZpad', [
		'nonce'       => wp_create_nonce( 'dasaweb-zpad' ),
		'speichert'   => __( 'speichere …', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'gespeichert' => __( 'gespeichert', 'dasaweb-zeige-pipeline-auf-dashboard' ),
		'fehler'      => __( 'konnte nicht gespeichert werden', 'dasaweb-zeige-pipeline-auf-dashboard' ),
	] );
	wp_add_inline_script( 'dasaweb-zpad', $js );
}


/**
 * AJAX: die gezogene Reihenfolge als Liste von Post-IDs im user_meta ablegen.
 */
function dasaweb_DashboardPipeline_reihenfolge_speichern(): void {

	check_ajax_referer( 'dasaweb-zpad', 'nonce' );

	if ( ! current_user_can( DASAWEB_ZPAD_CAP ) ) {
		wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
	}

	$roh = isset( $_POST['order'] ) ? (array) wp_unslash( $_POST['order'] ) : [];
	$ids = array_values( array_unique( array_filter( array_map( 'absint', $roh ) ) ) );

	update_user_meta( get_current_user_id(), DASAWEB_ZPAD_META, $ids );

	wp_send_json_success( [ 'count' => count( $ids ) ] );
}


/**
 * AJAX: den Fertigstellungsgrad eines einzelnen Entwurfs speichern.
 */
function dasaweb_DashboardPipeline_fortschritt_speichern(): void {

	check_ajax_referer( 'dasaweb-zpad', 'nonce' );

	$id = isset( $_POST['post'] ) ? absint( wp_unslash( $_POST['post'] ) ) : 0;

	// Neben dem allgemeinen Recht auch das für genau diesen Beitrag prüfen
	if ( ! $id || ! current_user_can( DASAWEB_ZPAD_CAP ) || ! current_user_can( 'edit_post', $id ) ) {
		wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
	}

	$wert = dasaweb_DashboardPipeline_fortschritt_pruefen(
		isset( $_POST['wert'] ) ? wp_unslash( $_POST['wert'] ) : 0
	);

	update_post_meta( $id, DASAWEB_ZPAD_FORTSCHRITT, $wert );

	wp_send_json_success( [ 'wert' => $wert ] );
}


/**
 * Sobald ein Beitrag erscheint, ist sein Fortschritt erledigt - der Wert fliegt raus.
 *
 * Bewusst erst bei 'publish' und nicht schon bei 'future': Wird ein geplanter
 * Artikel zurück in die Entwürfe geholt, steht sein Fortschritt noch da.
 */
function dasaweb_DashboardPipeline_fortschritt_aufraeumen( string $neu, string $alt, WP_Post $post ): void {

	if ( 'publish' === $neu && 'publish' !== $alt && 'post' === $post->post_type ) {
		delete_post_meta( $post->ID, DASAWEB_ZPAD_FORTSCHRITT );
	}
}


/**
 * Beim Löschen des Plugins alle Spuren entfernen: Reihenfolgen aller Nutzer
 * und die Fortschrittswerte aller Beiträge.
 */
function dasaweb_DashboardPipeline_deinstallieren(): void {

	delete_metadata( 'user', 0, DASAWEB_ZPAD_META, '', true );
	delete_metadata( 'post', 0, DASAWEB_ZPAD_FORTSCHRITT, '', true );
}

Wobei ich mir schon die Frage stelle: Wenn ich so etwas brauchen würde, würde ich es aktuell dann wirklich irgendwo suchen oder es mir einfach kurz bauen lassen, so wie ich es im Prinzip ja auch gemacht habe? Und: Sollte man einen Code dieser Art auf Github oder in das WordPress-Plugin-Verzeichnis legen? Sucht da noch jemand oder sind diese Zeiten vorbei?

0 Reaktionen auf “WordPress-Entwurfsmanagement”

  1. Daniel Weber

    Mein Blog war schon mal viel lebendiger, er war aber auch schon viel toter. Wie ich im Moment meine Blog-Entwürfe manage:

    #blog #wordpress

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert