Add an owner-only publish switch to each /trips card. It POSTs to a new
entry-actions route that mutates trip.md `published` and invalidates the
page-tree cache so the listing, nav and home reflect the change on the
next load. Owner sees drafts (Draft badge); anon/non-owner unchanged.
- U1 EntryScopeGuard::resolveTripChild — resolve a slug to a direct child
of /trips (drafts included, for republish)
- U2 POST /api/v1/trip/{slug}/publish (setTripPublished) — owner-gated
write, strict is_bool body, header-mutation save, audit log
- U3 trip-publish-toggle partial + CSS (role=switch, Draft badge, visible
failure toast, ≥44px target)
- U4 owner-aware /trips listing + card restructure (toggle overlays cover
as a non-anchor sibling; works for coverless drafts)
- U5 home active-trip branch falls back when the active trip is unpublished
- U6 trip-publish.js (confirm/pending/optimistic/revert) + esbuild wiring
Cache note: an in-place frontmatter edit keeps the folder-check cache id,
and driver:auto uses APCu in web memory, so deleteAll()+invalidateCache()
is insufficient — the endpoint also calls apcu_clear_cache().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
76 lines
3.5 KiB
PHP
76 lines
3.5 KiB
PHP
<?php
|
|
namespace Grav\Plugin;
|
|
|
|
use Grav\Common\Plugin;
|
|
use RocketTheme\Toolbox\Event\Event;
|
|
|
|
/**
|
|
* Entry Actions — a thin, purpose-built API surface for owner-only, active-trip
|
|
* scoped journal-entry actions that the stock Grav API cannot express safely.
|
|
*
|
|
* Routes:
|
|
* - DELETE /api/v1/entry/{slug} — delete a journal entry folder
|
|
* - POST /api/v1/entry/{slug}/photos/order — reorder an entry's photos
|
|
* - POST /api/v1/trip/{slug}/publish — publish/unpublish a trip
|
|
*
|
|
* The stock DELETE /api/v1/pages<route> only checks write-permission (no trip
|
|
* scope, and any admin passes), which violates R6; and no stock endpoint can
|
|
* rename media to the photo-NN cover order at all. Both custom routes require the
|
|
* configured site OWNER and assert the target is a direct child of the active
|
|
* trip's dailies container — sharing one guard (EntryScopeGuard) with the save
|
|
* path so the R6 enforcement points cannot diverge (KTD5).
|
|
*
|
|
* Custom-in-repo (NOT GPM-managed): tracked via a `!` negation in user/.gitignore
|
|
* and deployed with the content push, like cache-on-save. Never in plugins.txt.
|
|
*/
|
|
class EntryActionsPlugin extends Plugin
|
|
{
|
|
public static function getSubscribedEvents(): array
|
|
{
|
|
return [
|
|
'onPluginsInitialized' => ['onPluginsInitialized', 0],
|
|
'onApiRegisterRoutes' => ['onApiRegisterRoutes', 0],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Register a lazy PSR-4 autoloader for this plugin's classes. The api router
|
|
* dispatches from a CACHED route map and instantiates the controller directly
|
|
* (ApiRouter::handleRoute → `new $controllerClass`) WITHOUT re-firing
|
|
* onApiRegisterRoutes, so requiring the class only there would leave it
|
|
* unloaded on cached-route requests. Lazy autoloading fires exactly when the
|
|
* router constructs the controller — by which point the api plugin's own
|
|
* autoloader (for AbstractApiController) is already registered.
|
|
*/
|
|
public function onPluginsInitialized(): void
|
|
{
|
|
spl_autoload_register(static function (string $class): void {
|
|
$prefix = 'Grav\\Plugin\\EntryActions\\';
|
|
if (strncmp($class, $prefix, strlen($prefix)) !== 0) {
|
|
return;
|
|
}
|
|
$rel = substr($class, strlen($prefix));
|
|
$file = __DIR__ . '/classes/' . str_replace('\\', '/', $rel) . '.php';
|
|
if (is_file($file)) {
|
|
require_once $file;
|
|
}
|
|
});
|
|
}
|
|
|
|
public function onApiRegisterRoutes(Event $event): void
|
|
{
|
|
$routes = $event['routes'];
|
|
$routes->delete('/entry/{slug}', [EntryActions\EntryActionsApiController::class, 'deleteEntry']);
|
|
// Reorder an entry's photos to a client-supplied order → rename to
|
|
// photo-01..NN so the feed cover (media.images|first) follows the drag.
|
|
// Nested-static-after-param, same shape as the DELETE above — it only
|
|
// registers once the API route-map cache is rebuilt (deploy must clear cache).
|
|
$routes->post('/entry/{slug}/photos/order', [EntryActions\EntryActionsApiController::class, 'reorderPhotos']);
|
|
// Publish/unpublish a trip from the /trips listing → mutate trip.md
|
|
// `published` and invalidate the page-tree index. Owner-only, any trip
|
|
// (not active-scoped). Same registration caveat as above: it only takes
|
|
// effect once the API route-map cache is rebuilt (deploy must clear cache).
|
|
$routes->post('/trip/{slug}/publish', [EntryActions\EntryActionsApiController::class, 'setTripPublished']);
|
|
}
|
|
}
|