feat(post-form): U6 — owner-scoped delete API route + card wiring
New custom-in-repo plugin entry-actions (un-ignored in .gitignore, NOT in
plugins.txt) registers DELETE /api/v1/entry/{slug} via onApiRegisterRoutes
(KTD5). The handler requires the authenticated site OWNER (not any login/admin),
rejects unsafe slugs (400), resolves the target through the page tree, asserts it
is a direct child of the active trip's dailies container, deletes the folder and
clears the cache — sharing EntryScopeGuard with the save path so R6 can't diverge.
A lazy per-namespace autoloader loads the controller on cached-route requests
(the router dispatches from route.cache without re-firing onApiRegisterRoutes).
EntryScopeGuard gains isOwnerUser() (API user comes from the request, not
$grav['user']) and enablePages() before find() (pages are lazily disabled in the
API context).
feed-actions.js (new, built via make build-assets; loaded on the trip/home feed
only when owner_can_edit) wires the inline Delete → Cancel/Confirm swap: on
Confirm it locks both buttons (D2, no double-DELETE), fetches the route
(credentials:include), removes the card, moves focus to the next card, and
announces via a page-level aria-live region (D4); on failure it restores the
control with an inline message (D7). Adds .sr-only + .entry-action[hidden] CSS.
Verified on the 2.0.4 container — API matrix 8/8 (anon 401, non-owner 403, bad
slug 400, out-of-scope 404, owner 204 + folder removed; V3/V5) and the delete UI
in a headless browser (confirm swap, card removal, disk deletion, live announce).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
!/plugins/.gitkeep
|
!/plugins/.gitkeep
|
||||||
!/plugins/cache-on-save/
|
!/plugins/cache-on-save/
|
||||||
!/plugins/story-blocks/
|
!/plugins/story-blocks/
|
||||||
|
!/plugins/entry-actions/
|
||||||
/data/
|
/data/
|
||||||
/pages/01.trips/italy-2026-demo/
|
/pages/01.trips/italy-2026-demo/
|
||||||
/pages/02.post/*ui-test*/
|
/pages/02.post/*ui-test*/
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ class EntryScopeGuard
|
|||||||
if (!$user || empty($user->authenticated)) {
|
if (!$user || empty($user->authenticated)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return self::isOwnerUser($grav, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owner check for an explicit user object — used by the API delete route,
|
||||||
|
* whose authenticated user comes from the request (api_user attribute), not
|
||||||
|
* $grav['user']. Same rule: username must equal site.owner_username.
|
||||||
|
*/
|
||||||
|
public static function isOwnerUser(Grav $grav, $user): bool
|
||||||
|
{
|
||||||
|
if (!$user || !isset($user->username)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$owner = $grav['config']->get('site.owner_username');
|
$owner = $grav['config']->get('site.owner_username');
|
||||||
return is_string($owner) && $owner !== '' && $user->username === $owner;
|
return is_string($owner) && $owner !== '' && $user->username === $owner;
|
||||||
}
|
}
|
||||||
@@ -97,7 +110,14 @@ class EntryScopeGuard
|
|||||||
if ($dailies === null) {
|
if ($dailies === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$page = $grav['pages']->find($dailies . '/' . $segment);
|
$pages = $grav['pages'];
|
||||||
|
// In the API request context the page tree is lazily disabled; enable it
|
||||||
|
// so find() can resolve (mirrors the api plugin's own resolvePageByRoute).
|
||||||
|
// Idempotent — a no-op in the frontend save-path context.
|
||||||
|
if (method_exists($pages, 'enablePages')) {
|
||||||
|
$pages->enablePages();
|
||||||
|
}
|
||||||
|
$page = $pages->find($dailies . '/' . $segment);
|
||||||
if ($page === null) {
|
if ($page === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: Entry Actions
|
||||||
|
version: 0.1.0
|
||||||
|
description: Owner-only, active-trip-scoped journal entry actions (M1: delete) via the Grav API.
|
||||||
|
icon: trash
|
||||||
|
author:
|
||||||
|
name: Mischa
|
||||||
|
homepage: https://intotheeast.com
|
||||||
|
keywords: api, journal, delete
|
||||||
|
bugs: https://intotheeast.com
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- { name: grav, version: '>=2.0.0' }
|
||||||
|
- { name: api }
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
namespace Grav\Plugin\EntryActions;
|
||||||
|
|
||||||
|
use Grav\Common\Filesystem\Folder;
|
||||||
|
use Grav\Plugin\Api\Controllers\AbstractApiController;
|
||||||
|
use Grav\Plugin\Api\Exceptions\ApiException;
|
||||||
|
use Grav\Plugin\Api\Exceptions\ForbiddenException;
|
||||||
|
use Grav\Plugin\Api\Exceptions\NotFoundException;
|
||||||
|
use Grav\Plugin\Api\Response\ApiResponse;
|
||||||
|
use Grav\Plugin\Shared\EntryScopeGuard;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
// Shared R6 guard lives in cache-on-save (the always-present custom plugin);
|
||||||
|
// require it so save and delete enforce scope identically (KTD5).
|
||||||
|
require_once dirname(__DIR__, 2) . '/cache-on-save/classes/EntryScopeGuard.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/v1/entry/{slug}
|
||||||
|
*
|
||||||
|
* Deletes a journal entry folder, but only when ALL hold:
|
||||||
|
* - the request is the authenticated site OWNER (not merely any login/admin);
|
||||||
|
* - {slug} is a safe single segment (no '/', no '..');
|
||||||
|
* - it resolves through the page tree to a DIRECT child of the ACTIVE trip's
|
||||||
|
* dailies container.
|
||||||
|
* Otherwise: 401 (anon), 403 (non-owner), 400 (bad slug), 404 (out of scope /
|
||||||
|
* not found). On success the folder is removed and the page-tree cache cleared.
|
||||||
|
*/
|
||||||
|
class EntryActionsApiController extends AbstractApiController
|
||||||
|
{
|
||||||
|
public function deleteEntry(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
// Authenticated OWNER only (KTD8). getUser() throws 401 for anonymous.
|
||||||
|
$user = $this->getUser($request);
|
||||||
|
if (!EntryScopeGuard::isOwnerUser($this->grav, $user)) {
|
||||||
|
throw new ForbiddenException('Only the site owner can delete journal entries.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $this->getRouteParam($request, 'slug');
|
||||||
|
if (!is_string($slug) || !EntryScopeGuard::isSafeSegment($slug)) {
|
||||||
|
throw new ApiException(400, 'Bad Request', 'Invalid entry slug.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve via $pages->find() + parent-route assertion (never raw path
|
||||||
|
// concatenation) — same shared check the save path uses.
|
||||||
|
$page = EntryScopeGuard::resolveActiveDailyChild($this->grav, $slug);
|
||||||
|
if ($page === null) {
|
||||||
|
throw new NotFoundException('Entry not found in the active trip.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $page->path();
|
||||||
|
if (!is_string($path) || $path === '' || !is_dir($path)) {
|
||||||
|
throw new NotFoundException('Entry folder not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Folder::delete($path);
|
||||||
|
$this->grav['cache']->deleteAll();
|
||||||
|
|
||||||
|
return ApiResponse::noContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?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.
|
||||||
|
*
|
||||||
|
* M1 registers exactly one route: DELETE /api/v1/entry/{slug}. The stock
|
||||||
|
* DELETE /api/v1/pages<route> only checks write-permission (no trip scope, and
|
||||||
|
* any admin passes), which violates R6. This route requires the configured site
|
||||||
|
* OWNER and asserts the target is a direct child of the active trip's dailies
|
||||||
|
* container — sharing one guard (EntryScopeGuard) with the save path so the two
|
||||||
|
* 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
enabled: true
|
||||||
@@ -329,6 +329,9 @@ body::after {
|
|||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
/* [hidden] must win over the inline-flex display above (used to swap
|
||||||
|
Delete <-> Cancel/Confirm from feed-actions.js). */
|
||||||
|
.entry-action[hidden] { display: none; }
|
||||||
.entry-action--confirm {
|
.entry-action--confirm {
|
||||||
color: #E5786A; /* soft red — destructive confirm */
|
color: #E5786A; /* soft red — destructive confirm */
|
||||||
border-color: #E5786A;
|
border-color: #E5786A;
|
||||||
@@ -350,6 +353,19 @@ body::after {
|
|||||||
}
|
}
|
||||||
.entry-delete-msg:empty { display: none; }
|
.entry-delete-msg:empty { display: none; }
|
||||||
|
|
||||||
|
/* Visually-hidden but screen-reader-available (feed-actions live region). */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.journal-photo-wrap {
|
.journal-photo-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: var(--space-5);
|
margin-bottom: var(--space-5);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
(()=>{function m(){var e=document.getElementById("feed-actions-live");return e||(e=document.createElement("div"),e.id="feed-actions-live",e.setAttribute("aria-live","polite"),e.setAttribute("role","status"),e.className="sr-only",document.body.appendChild(e)),e}function s(e){m().textContent=e}function v(e){var n=String(e||"").replace(/\/+$/,"").split("/");return n[n.length-1]||""}function u(e,n){var i=e.querySelector("[data-delete-start]"),r=e.querySelector(".entry-delete-confirm");if(i&&(i.hidden=n),r&&(r.hidden=!n),!n){var t=e.querySelector(".entry-delete-msg");t&&(t.textContent="")}}function g(e){var n=e.getAttribute("data-entry-route"),i=v(n),r=e.querySelector("[data-delete-cancel]"),t=e.querySelector("[data-delete-confirm]"),a=e.querySelector(".entry-delete-msg"),l=e.closest(".journal-post");t&&(t.disabled=!0,t.textContent="Deleting\u2026"),r&&(r.disabled=!0),a&&(a.textContent=""),s("Deleting entry\u2026"),fetch("/api/v1/entry/"+encodeURIComponent(i),{method:"DELETE",credentials:"include",headers:{Accept:"application/json"}}).then(function(d){if(!(d.status===204||d.ok))throw new Error("HTTP "+d.status);for(var o=l?l.nextElementSibling:null;o&&!o.classList.contains("journal-post");)o=o.nextElementSibling;var c=o||(l&&l.previousElementSibling&&l.previousElementSibling.classList&&l.previousElementSibling.classList.contains("journal-post")?l.previousElementSibling:null)||document.querySelector(".home-trip-name, .home-feed-col h1, .feed");if(l&&l.remove(),c){c.hasAttribute("tabindex")||c.setAttribute("tabindex","-1");try{c.focus({preventScroll:!1})}catch{}}s("Entry deleted.")}).catch(function(){t&&(t.disabled=!1,t.textContent="Confirm delete"),r&&(r.disabled=!1),a&&(a.textContent="Could not delete \u2014 please try again."),s("Delete failed.")})}function f(){document.addEventListener("click",function(e){var n=e.target.closest?e.target.closest("[data-delete-start]"):null,i=e.target.closest?e.target.closest("[data-delete-cancel]"):null,r=e.target.closest?e.target.closest("[data-delete-confirm]"):null;if(!(!n&&!i&&!r)){var t=(n||i||r).closest(".journal-post-actions");if(t){if(e.preventDefault(),n){u(t,!0);return}if(i){u(t,!1);return}if(r){var a=t.querySelector("[data-delete-confirm]");if(a&&a.disabled)return;g(t)}}}})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",f):f();})();
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* feed-actions.js (U6) — owner delete wiring for journal feed cards.
|
||||||
|
*
|
||||||
|
* Loaded only on the trip / home active-trip feed for the owner. The Edit link is
|
||||||
|
* a plain navigation (no JS). Delete is a two-step inline confirm (no browser
|
||||||
|
* dialog): Delete → Cancel / Confirm delete → DELETE /api/v1/entry/<slug>
|
||||||
|
* (session cookie), then the card is removed and focus moves to the next card.
|
||||||
|
*
|
||||||
|
* Markup (rendered by partials/entry-journal.html.twig):
|
||||||
|
* article.journal-post[data-entry-route]
|
||||||
|
* .journal-post-actions[data-entry-route]
|
||||||
|
* button[data-delete-start] (Delete)
|
||||||
|
* .entry-delete-confirm[hidden]
|
||||||
|
* button[data-delete-cancel] (Cancel)
|
||||||
|
* button[data-delete-confirm] (Confirm delete)
|
||||||
|
* .entry-delete-msg[aria-live] (inline error slot)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// One page-level polite live region for cross-card announcements (D4) — the
|
||||||
|
// per-card .entry-delete-msg vanishes with the card it belongs to.
|
||||||
|
function liveRegion() {
|
||||||
|
var el = document.getElementById('feed-actions-live');
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.id = 'feed-actions-live';
|
||||||
|
el.setAttribute('aria-live', 'polite');
|
||||||
|
el.setAttribute('role', 'status');
|
||||||
|
el.className = 'sr-only';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
function announce(msg) { liveRegion().textContent = msg; }
|
||||||
|
|
||||||
|
function slugFromRoute(route) {
|
||||||
|
var parts = String(route || '').replace(/\/+$/, '').split('/');
|
||||||
|
return parts[parts.length - 1] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showConfirm(actions, show) {
|
||||||
|
var del = actions.querySelector('[data-delete-start]');
|
||||||
|
var confirm = actions.querySelector('.entry-delete-confirm');
|
||||||
|
if (del) del.hidden = show;
|
||||||
|
if (confirm) confirm.hidden = !show;
|
||||||
|
if (!show) {
|
||||||
|
var msg = actions.querySelector('.entry-delete-msg');
|
||||||
|
if (msg) msg.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function performDelete(actions) {
|
||||||
|
var route = actions.getAttribute('data-entry-route');
|
||||||
|
var slug = slugFromRoute(route);
|
||||||
|
var cancelBtn = actions.querySelector('[data-delete-cancel]');
|
||||||
|
var confirmBtn = actions.querySelector('[data-delete-confirm]');
|
||||||
|
var msg = actions.querySelector('.entry-delete-msg');
|
||||||
|
var card = actions.closest('.journal-post');
|
||||||
|
|
||||||
|
// D2: lock both buttons immediately so a mobile double-tap can't fire two
|
||||||
|
// DELETEs (the second would 404 on an already-removed folder).
|
||||||
|
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = 'Deleting…'; }
|
||||||
|
if (cancelBtn) cancelBtn.disabled = true;
|
||||||
|
if (msg) msg.textContent = '';
|
||||||
|
announce('Deleting entry…');
|
||||||
|
|
||||||
|
fetch('/api/v1/entry/' + encodeURIComponent(slug), {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
|
}).then(function (r) {
|
||||||
|
if (!(r.status === 204 || r.ok)) throw new Error('HTTP ' + r.status);
|
||||||
|
|
||||||
|
// D4: move focus to the next journal card (or the feed heading) BEFORE
|
||||||
|
// removing this one, then announce.
|
||||||
|
var next = card ? card.nextElementSibling : null;
|
||||||
|
while (next && !next.classList.contains('journal-post')) next = next.nextElementSibling;
|
||||||
|
var focusTarget = next
|
||||||
|
|| (card && card.previousElementSibling && card.previousElementSibling.classList && card.previousElementSibling.classList.contains('journal-post') ? card.previousElementSibling : null)
|
||||||
|
|| document.querySelector('.home-trip-name, .home-feed-col h1, .feed');
|
||||||
|
if (card) card.remove();
|
||||||
|
if (focusTarget) {
|
||||||
|
if (!focusTarget.hasAttribute('tabindex')) focusTarget.setAttribute('tabindex', '-1');
|
||||||
|
try { focusTarget.focus({ preventScroll: false }); } catch (e) { /* older browsers */ }
|
||||||
|
}
|
||||||
|
announce('Entry deleted.');
|
||||||
|
}).catch(function () {
|
||||||
|
// D7: restore the control + inline error, constrained to the card.
|
||||||
|
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = 'Confirm delete'; }
|
||||||
|
if (cancelBtn) cancelBtn.disabled = false;
|
||||||
|
if (msg) msg.textContent = 'Could not delete — please try again.';
|
||||||
|
announce('Delete failed.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFeedActions() {
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var start = e.target.closest ? e.target.closest('[data-delete-start]') : null;
|
||||||
|
var cancel = e.target.closest ? e.target.closest('[data-delete-cancel]') : null;
|
||||||
|
var confirm = e.target.closest ? e.target.closest('[data-delete-confirm]') : null;
|
||||||
|
if (!start && !cancel && !confirm) return;
|
||||||
|
|
||||||
|
var actions = (start || cancel || confirm).closest('.journal-post-actions');
|
||||||
|
if (!actions) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (start) { showConfirm(actions, true); return; }
|
||||||
|
if (cancel) { showConfirm(actions, false); return; }
|
||||||
|
if (confirm) {
|
||||||
|
var btn = actions.querySelector('[data-delete-confirm]');
|
||||||
|
if (btn && btn.disabled) return; // already in flight (D2)
|
||||||
|
performDelete(actions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initFeedActions);
|
||||||
|
} else {
|
||||||
|
initFeedActions();
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/gen-weather-icons.js && esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && rm -rf js/post && esbuild js/src/post-form.js --bundle --minify --format=esm --splitting --outdir=js/post && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; } && { mv js/post/post-form.css css-compiled/post-form.css 2>/dev/null || true; }"
|
"build": "node scripts/gen-weather-icons.js && esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && esbuild js/src/feed-actions.js --bundle --minify --format=iife --outfile=js/feed-actions.js && rm -rf js/post && esbuild js/src/post-form.js --bundle --minify --format=esm --splitting --outdir=js/post && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; } && { mv js/post/post-form.css css-compiled/post-form.css 2>/dev/null || true; }"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/dm-sans": "latest",
|
"@fontsource-variable/dm-sans": "latest",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
and grav.user.username == grav.config.site.owner_username %}
|
and grav.user.username == grav.config.site.owner_username %}
|
||||||
{# Owner-aware feed list: owner sees drafts; everyone else published only #}
|
{# Owner-aware feed list: owner sees drafts; everyone else published only #}
|
||||||
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
|
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
|
||||||
|
{% if owner_can_edit %}{% do assets.addJs('theme://js/feed-actions.js', {group: 'bottom'}) %}{% endif %}
|
||||||
|
|
||||||
{% set all_items = [] %}
|
{% set all_items = [] %}
|
||||||
{% for e in journal_feed %}
|
{% for e in journal_feed %}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
{# Feed list is owner-aware: the owner sees drafts (unpublished) too; everyone
|
{# Feed list is owner-aware: the owner sees drafts (unpublished) too; everyone
|
||||||
else (and every non-active-trip view) sees published only (R5, KTD7). #}
|
else (and every non-active-trip view) sees published only (R5, KTD7). #}
|
||||||
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
|
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
|
||||||
|
{% if owner_can_edit %}{% do assets.addJs('theme://js/feed-actions.js', {group: 'bottom'}) %}{% endif %}
|
||||||
|
|
||||||
{% set all_items = [] %}
|
{% set all_items = [] %}
|
||||||
{% for e in journal_feed %}
|
{% for e in journal_feed %}
|
||||||
|
|||||||
Reference in New Issue
Block a user