fix(trips): remaining publish-toggle review findings
- resolveTripChild now asserts the resolved page uses the trip template, so a non-trip direct child of /trips could never be toggled through this endpoint (P3 adversarial). - apiSend gains an optional timeoutMs (AbortController); trip-publish passes 10s so a hung toggle can't leave the switch stuck aria-busy. post-form omits it, keeping media uploads unbounded (P2 reliability). - trips.html.twig reuses trip.html.twig's one-line active-trip slug match instead of a bespoke 3-branch OR (P2 maintainability). - Draft-badge amber is now a --color-draft-accent token shared by the trip and journal badges instead of a twice-hardcoded #E0A458 literal (P3). Rebuilt js/trip-publish.js and js/post/post-form.js (shared api-utils change). PHP lint clean; trip-publish suite 10/10; post suite unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
This commit is contained in:
@@ -289,8 +289,8 @@ body::after {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
color: #E0A458; /* warm amber — draft/unpublished */
|
||||
border: 1px solid #E0A458;
|
||||
color: var(--color-draft-accent); /* warm amber — draft/unpublished */
|
||||
border: 1px solid var(--color-draft-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.1em 0.5em;
|
||||
line-height: 1.5;
|
||||
@@ -1315,9 +1315,9 @@ body::after {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
color: #E0A458; /* warm amber — matches .journal-draft-badge */
|
||||
color: var(--color-draft-accent); /* warm amber — matches .journal-draft-badge */
|
||||
background: var(--color-canvas);
|
||||
border: 1px solid #E0A458;
|
||||
border: 1px solid var(--color-draft-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.15em 0.5em;
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
--color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover */
|
||||
--color-ink-inverse: #17171A; /* text on accent-coloured buttons */
|
||||
--color-error: #c0392b; /* validation errors, form error status */
|
||||
--color-draft-accent: #E0A458; /* warm amber — draft/unpublished badges */
|
||||
|
||||
/* ── Glass overlays (paper colour at opacity, for story components) ── */
|
||||
--color-paper-glass-low: color-mix(in srgb, var(--color-paper) 8%, transparent);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,12 +12,25 @@
|
||||
// generic failure. `okStatuses` lists extra codes to accept as success (e.g.
|
||||
// 204 no-content, or 404 already-gone for an idempotent DELETE). Cookies
|
||||
// auto-included.
|
||||
export function apiSend(url, opts, okStatuses) {
|
||||
return fetch(url, Object.assign({ credentials: 'include' }, opts)).then(function (r) {
|
||||
//
|
||||
// `timeoutMs` is OPTIONAL: when set, a hung request is aborted after that many
|
||||
// ms so the caller's pending state can't stick forever (the abort rejects like
|
||||
// any network error → generic error copy). Omit it (post-form's media uploads)
|
||||
// to keep the old unbounded behaviour — a large upload must not be timed out.
|
||||
export function apiSend(url, opts, okStatuses, timeoutMs) {
|
||||
var controller = timeoutMs ? new AbortController() : null;
|
||||
var timer = controller ? setTimeout(function () { controller.abort(); }, timeoutMs) : null;
|
||||
var fetchOpts = Object.assign({ credentials: 'include' }, opts);
|
||||
if (controller) fetchOpts.signal = controller.signal;
|
||||
return fetch(url, fetchOpts).then(function (r) {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (r.ok || (okStatuses && okStatuses.indexOf(r.status) !== -1)) return r;
|
||||
var e = new Error('HTTP ' + r.status);
|
||||
e.status = r.status;
|
||||
throw e;
|
||||
}, function (err) {
|
||||
if (timer) clearTimeout(timer);
|
||||
throw err; // abort (no .status) or network error → generic fallback copy
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { apiSend, apiErrorMsg } from './api-utils.js';
|
||||
|
||||
var TOAST_TIMEOUT_MS = 5000;
|
||||
var PUBLISH_TIMEOUT_MS = 10000; // abort a hung toggle so the switch never sticks (R13)
|
||||
var toastTimer = null;
|
||||
|
||||
// One visible, page-level polite toast (R15). Modelled on the feed-actions.js
|
||||
@@ -103,7 +104,7 @@ function onToggle(btn) {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ published: next })
|
||||
}).then(function () {
|
||||
}, null, PUBLISH_TIMEOUT_MS).then(function () {
|
||||
// Success: flip the switch + Draft badge in place, no reload (R14).
|
||||
setPublishedUI(btn, next);
|
||||
setPending(btn, false);
|
||||
|
||||
@@ -1 +1 @@
|
||||
(()=>{function s(t,e,i){return fetch(t,Object.assign({credentials:"include"},e)).then(function(r){if(r.ok||i&&i.indexOf(r.status)!==-1)return r;var n=new Error("HTTP "+r.status);throw n.status=r.status,n})}function o(t,e){var i=t&&t.status;return i===401||i===403?"Your login session expired \u2014 sign in again, then retry.":e}var p=5e3,a=null;function c(){var t=document.getElementById("trip-publish-live");if(!t){t=document.createElement("div"),t.id="trip-publish-live",t.className="trip-publish-toast",t.setAttribute("role","status"),t.setAttribute("aria-live","polite"),t.hidden=!0;var e=document.createElement("span");e.className="trip-publish-toast__msg";var i=document.createElement("button");i.type="button",i.className="trip-publish-toast__close",i.setAttribute("aria-label","Dismiss"),i.textContent="\xD7",i.addEventListener("click",d),t.appendChild(e),t.appendChild(i),document.body.appendChild(t)}return t}function f(t){var e=c();e.querySelector(".trip-publish-toast__msg").textContent=t,e.hidden=!1,a&&clearTimeout(a),a=setTimeout(d,p)}function d(){var t=document.getElementById("trip-publish-live");t&&(t.hidden=!0),a&&(clearTimeout(a),a=null)}function h(t,e){t.setAttribute("aria-checked",e?"true":"false"),t.setAttribute("data-published",e?"true":"false");var i=t.closest(".trip-publish-overlay"),r=i?i.querySelector(".trip-draft-badge"):null;r&&(r.hidden=e)}function u(t,e){e?(t.setAttribute("aria-busy","true"),t.disabled=!0):(t.removeAttribute("aria-busy"),t.disabled=!1)}function m(t){if(t.getAttribute("aria-busy")!=="true"){var e=t.getAttribute("data-published")==="true",i=!e;if(!(!i&&t.getAttribute("data-active")==="true"&&!window.confirm("This is your active trip \u2014 unpublishing it also removes it from the home page. Unpublish anyway?"))){var r=t.getAttribute("data-trip-slug");u(t,!0),s("/api/v1/trip/"+encodeURIComponent(r)+"/publish",{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({published:i})}).then(function(){h(t,i),u(t,!1)}).catch(function(n){u(t,!1),f(o(n,"Couldn't update \u2014 try again."))})}}}function l(){document.addEventListener("click",function(t){var e=t.target.closest?t.target.closest(".trip-publish-toggle"):null;e&&(t.preventDefault(),m(e))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",l):l();})();
|
||||
(()=>{function c(t,e,i,r){var n=r?new AbortController:null,s=n?setTimeout(function(){n.abort()},r):null,l=Object.assign({credentials:"include"},e);return n&&(l.signal=n.signal),fetch(t,l).then(function(a){if(s&&clearTimeout(s),a.ok||i&&i.indexOf(a.status)!==-1)return a;var d=new Error("HTTP "+a.status);throw d.status=a.status,d},function(a){throw s&&clearTimeout(s),a})}function p(t,e){var i=t&&t.status;return i===401||i===403?"Your login session expired \u2014 sign in again, then retry.":e}var m=5e3,v=1e4,u=null;function g(){var t=document.getElementById("trip-publish-live");if(!t){t=document.createElement("div"),t.id="trip-publish-live",t.className="trip-publish-toast",t.setAttribute("role","status"),t.setAttribute("aria-live","polite"),t.hidden=!0;var e=document.createElement("span");e.className="trip-publish-toast__msg";var i=document.createElement("button");i.type="button",i.className="trip-publish-toast__close",i.setAttribute("aria-label","Dismiss"),i.textContent="\xD7",i.addEventListener("click",h),t.appendChild(e),t.appendChild(i),document.body.appendChild(t)}return t}function b(t){var e=g();e.querySelector(".trip-publish-toast__msg").textContent=t,e.hidden=!1,u&&clearTimeout(u),u=setTimeout(h,m)}function h(){var t=document.getElementById("trip-publish-live");t&&(t.hidden=!0),u&&(clearTimeout(u),u=null)}function T(t,e){t.setAttribute("aria-checked",e?"true":"false"),t.setAttribute("data-published",e?"true":"false");var i=t.closest(".trip-publish-overlay"),r=i?i.querySelector(".trip-draft-badge"):null;r&&(r.hidden=e)}function o(t,e){e?(t.setAttribute("aria-busy","true"),t.disabled=!0):(t.removeAttribute("aria-busy"),t.disabled=!1)}function y(t){if(t.getAttribute("aria-busy")!=="true"){var e=t.getAttribute("data-published")==="true",i=!e;if(!(!i&&t.getAttribute("data-active")==="true"&&!window.confirm("This is your active trip \u2014 unpublishing it also removes it from the home page. Unpublish anyway?"))){var r=t.getAttribute("data-trip-slug");o(t,!0),c("/api/v1/trip/"+encodeURIComponent(r)+"/publish",{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({published:i})},null,v).then(function(){T(t,i),o(t,!1)}).catch(function(n){o(t,!1),b(p(n,"Couldn't update \u2014 try again."))})}}}function f(){document.addEventListener("click",function(t){var e=t.target.closest?t.target.closest(".trip-publish-toggle"):null;e&&(t.preventDefault(),y(e))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",f):f();})();
|
||||
|
||||
@@ -19,13 +19,12 @@
|
||||
{% set stories_page = grav.pages.find(trip.route ~ '/stories') %}
|
||||
{% set journal_count = dailies_page ? dailies_page.children.published()|length : 0 %}
|
||||
{% set story_count = stories_page ? stories_page.children.published()|length : 0 %}
|
||||
{# Robust active-trip match: site.active_trip may be a full route
|
||||
(/trips/x) or a bare slug — normalise both sides before comparing so the
|
||||
R12 confirm never silently drops (both forms exist in helpers.js /
|
||||
cache-on-save). #}
|
||||
{% set active = (config.site.active_trip ?? '')|trim('/') %}
|
||||
{% set trip_route = trip.route|trim('/') %}
|
||||
{% set is_active = active != '' and (active == trip_route or active == ('trips/' ~ trip.slug) or active == trip.slug) %}
|
||||
{# Active-trip match: site.active_trip may be a full route (/trips/x) or a
|
||||
bare slug — normalise to its last segment and compare to the trip slug,
|
||||
the same one-liner trip.html.twig uses, so the R12 confirm never silently
|
||||
drops. #}
|
||||
{% set active_trip_slug = (grav.config.site.active_trip|default(''))|split('/')|last %}
|
||||
{% set is_active = active_trip_slug != '' and trip.slug == active_trip_slug %}
|
||||
{# The wrapper is a positioned container so the owner toggle can overlay the
|
||||
cover as a SIBLING of the navigating <a> (KTD6) — and it exists even for a
|
||||
coverless draft, giving the toggle an anchor whether or not the cover macro
|
||||
|
||||
Reference in New Issue
Block a user