Skip to content

Add DA folder picker and per-item image upload to Homepage configurator - #244

Merged
sharmeebuilds merged 40 commits into
homepage-configuratorfrom
homepage-configurator-q
Aug 14, 2026
Merged

Add DA folder picker and per-item image upload to Homepage configurator#244
sharmeebuilds merged 40 commits into
homepage-configuratorfrom
homepage-configurator-q

Conversation

@qiyundai

@qiyundai qiyundai commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an end-to-end image + link-based authoring flow for Homepage session
surfaces (Upcoming Sessions and Featured Sessions) in the Tier 1 Event
Configurator, wires up the actual rendering blocks to consume it, and
refreshes the configurator's own UI to align with Spectrum S2.

Authoring (Tier 1 Event Configurator, Homepage editor)

  • Per-session image upload via a two-phase modal: browse/create a DA folder
    (column browser + breadcrumbs; "Add folder" is purely client-side —
    DA has no folder-creation endpoint, so a typed name only becomes a real
    folder once the first upload lands in it), then pick a JPG and upload.
    Defaults to the event's own conventional folder
    (events/homepage-assets/<eventId>) so doing nothing still lands
    somewhere organized, but authors can redirect elsewhere. Uploads are
    previewed and published via the Helix/AEM admin API, so the field ends
    up with a real, live .aem.live URL, not a raw content.da.live link.
  • A "Section heading" field per config, and "Copy Link" replaces the old
    "Copy JSON" output: a single link (mirroring Schedule Maker's pattern)
    encodes the full session list — including each session's image, watch
    URL, and Mobile Rider stream ID — as a base64 payload in the link's URL
    hash, prefixed with the config type and a timestamp in its link text.
    Authors paste that link directly into the target page's doc body.
    Clicking the link also reopens the configurator on that exact config.
  • Visual refresh to Spectrum S2: the app's --tec-* custom properties now
    resolve to --s2a-* design tokens instead of hardcoded hex, matching
    tokens already established elsewhere in this repo. Also: no more 960px
    content-width cap, fully pill-shaped buttons, a wider/more prominent
    "Section heading" input, a vertically-centered editor back-button, and a
    toast redesigned to drop its non-Spectrum left-border stripe in favor of
    solid variant backgrounds.

Rendering (event-libs blocks)

  • decorate.js gained a tec-homepage auto-block builder (alongside the
    existing chrono-box/sessions-guide ones): it finds the pasted link,
    decodes its payload, and replaces it with either an upcoming-sessions
    or a featured-sessions block, carrying the decoded config as a data
    attribute.
  • upcoming-sessions.js reads that config directly and renders each
    session's image with its own carousel controls.
  • featured-sessions builds cards fully from the payload, reusing the
    existing generic event-card block's own media/body-building and
    session-routing logic rather than duplicating it — and now renders as a
    real horizontally-scrolling carousel (matching every "Featured
    Sessions"-style section elsewhere on the team's own demo doc) by reusing
    event-carousel.js's track/arrow logic directly, instead of the plain
    wrapping grid (no controls) it originally shipped as. This retires the
    old hydrate/featured-sessions-classname/session-code mechanism
    (hydrate/event-card.js, deleted), which required one hand-placed
    event-card per session and never supported images at all.

Review notes

Went through several rounds of independent review (correctness, security,
simplicity) across this branch's history. Fixes from the latest round:

  • Security (Medium): hasUnsafePathSegment's path-traversal guard only
    rejected literal ./.. segments; a percent-encoded equivalent
    (%2e%2e) could bypass it since neither listFolder nor uploadMedia
    decode the path before use. It now decodes each segment first (treating
    an undecodable segment as unsafe). The restored folder-name input is
    also now restricted client-side to [A-Za-z0-9._-].
  • Correctness: removing hardcoded font-family so global rules cascade
    through was correct for <p>/<a> elements (which inherit normally),
    but broke .carousel-pill, a <button> — buttons don't inherit
    font-family from ancestors by browser default. Fixed with
    font-family: inherit (not a hardcoded name).
  • Correctness: an undocumented padding had been added to
    .featured-sessions alongside its position: relative stacking-context
    fix, violating this repo's own documented rule against hardcoding block
    spacing without justification (MWPW-201396). Removed, keeping only the
    positioning fix.
  • Card titles/tracks/heading (and the config-driven featured-sessions
    heading) are set via .textContent rather than createTag's
    insertAdjacentHTML-backed html argument, since that data comes from a
    decoded link payload rather than hand-authored DA content.
  • A card's CTA href is sanitized through the existing safeUrl()
    allowlist before being set.

Noted as follow-ups, not addressed in this PR (real but out of scope
for a clean, bounded change):

  • ImagePickerModal.js's restored folder browser substantially overlaps
    schedule-maker/components/editor/FragmentPathBrowser.js (same state
    shape, identical helper functions). Not a clean drop-in extraction —
    schedule-maker's version is file-selection-oriented with deep-link
    auto-expansion and no folder-creation, this one is folder-only with
    pseudo-create — but a shared, parameterized component is worth designing
    separately.
  • featured-sessions.js reuses event-carousel.js via a synthetic empty
    marker element routed through its full init() pipeline, rather than
    event-carousel.js exporting the 2-3 helpers actually needed (e.g.
    buildArrows). Works correctly as-is; the more direct alternative would
    change event-carousel.js's export surface, which the manually-authored
    block flow also depends on.

Test plan

  • npm run lint
  • npm test (full suite passing, 1465 tests)
  • Load the app locally per README's ?ref=local DA proxy flow, sign in
    at da.live
  • In the Homepage editor (either config type): upload a JPG image for a
    session via the restored folder picker — browse an existing folder,
    create a new one, confirm it lands in DA, previews, and publishes;
    set a section heading; Copy Link
  • Paste the copied link into a real page's doc body, confirm on preview
    that the full card list renders with no per-card authoring, the
    heading is visible (not hidden below 900px), each session's image
    shows up, and Featured Sessions now scrolls as a carousel with
    working arrows
  • Click the copied link directly and confirm it reopens the
    configurator on that exact config

🤖 Generated with Claude Code

qiyundai and others added 29 commits August 11, 2026 17:51
Introduces a parallel, opt-in layout system for event pages: setting
page metadata `section-layout: columns` makes the page's <main> a flex
row of its top-level .section children (columns), each still stacking
blocks normally inside. Below 900px, columns collapse back to normal
vertical stacking. Per-column width ratios are authored via the
existing Section Metadata block's generic `style` key
(column-span-1/2/3), reusing Milo's existing class-attachment
mechanism with zero changes to Milo. The new applySectionColumnsLayout
helper is idempotent and always resolves the real page <main>, since
decorateEvent re-enters for fragments/personalization/events-form with
non-<main> parents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
applySectionColumnsLayout now toggles the class based on a fresh
getMetadata() read instead of a one-time "checked" flag, so a
personalization pass that updates section-layout metadata after the
first decorateEvent call still takes effect. Also drops the redundant
column-span-1 CSS rule, which just duplicated the existing default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
decorateEvent only runs on pages with event-id, so its internal call to
applySectionColumnsLayout() never reached non-event pages. Consuming
sites (e.g. da-events) need to call it directly and unconditionally
from their own decorateArea instead. Export it (and no longer call it
from decorateEvent) via the libs.js barrel, and make it load its own
required CSS internally so callers only need this one function instead
of having to remember to pair it with addStylesToEventPage().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the "all sections become equal columns" behavior with
selective per-row grouping: sections default to full width (flex: 1 1
100%, forcing each onto its own line, identical to normal stacking).
Only sections tagged column-span-1/2/3 shrink from 100% and pack onto
a shared line with their contiguous column-span-tagged neighbors. An
untagged section can never fit alongside anything else, so it always
forces a line break before and after itself — grouping is inherently
contiguous by construction, with no validation logic needed.

Uses flex-wrap instead of CSS Grid specifically because flexbox
recalculates fill-percentage per line independently, so a group's
flex-grow ratios always sum to fill their shared row completely
regardless of how many items are in it — a fixed-track grid would
leave a visible gap when a group's spans don't sum to the template's
column count.

No JS or test changes: applySectionColumnsLayout() only ever toggled
a class and ensured the stylesheet loads; all grouping logic now lives
entirely in this CSS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…match

Split the column-span-1/2/3 rules' min-width declaration onto its own
selector block (declaration-block-single-line-max-declarations was
being violated by packing flex + min-width onto one line).

Also normalize the section-layout metadata comparison with
.trim().toLowerCase(), matching how other author-facing metadata
values are already handled elsewhere in this file (e.g. the theme/
style comparisons) — content authors type this value directly, so it
shouldn't silently no-op on stray capitalization or whitespace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds docs/section-columns-layout.md following this repo's existing
authoring-guidelines doc style (see track-icon-config-authoring-
guidelines.md): the metadata contract, how to group sections into a
row via column-span-1/2/3, why grouping is inherently contiguous, and
the technical rationale for the no-DOM-reparenting, flex-wrap-based
design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flexbox (and CSS Grid) default align-items to stretch, which was
making every section in a shared row match the height of its tallest
sibling. Set align-items: flex-start on main.section-columns so each
section keeps sizing to its own content, the same way it does as a
normal direct child of <main> outside this feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revert to align-items: stretch on main.section-columns. This equalizes
section height within a shared row (useful for backgrounds/borders
lining up cleanly) without stretching content inside, since .section
lays out its children as normal block flow rather than flex/grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… rule

Adds to the authoring guide: the general "own weight / row's total
weight" ratio formula (with the 60/40 and thirds examples), that any
number of contiguous tagged sections can share a row (not just two),
and that two distinct back-to-back groups need an untagged divider
section between them since only a full-width section forces a line
break. Includes a second worked example with multiple independent
rows, a solo full-width section, and a 3-up row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SESSION_CODE_PATTERN only matched an "S" prefix (e.g. s6304), so
session codes with other RainFocus prefixes (OS565, AL687, ...)
fell through to the metadata-key branch instead, overwriting the
real featured-sessions key and silently failing to hydrate that
card's tokens.

Broadens the pattern to any 1-4 letter prefix + digits, and adds a
regression test for an OS-prefixed code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SESSION_CODE_PATTERN only matched an "S" prefix (e.g. s6304), so
session codes with other RainFocus prefixes (OS565, AL687, ...)
fell through to the metadata-key branch instead, overwriting the
real featured-sessions key and silently failing to hydrate that
card's tokens.

Broadens the pattern to any 1-4 letter prefix + digits, and adds a
regression test for an OS-prefixed code.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Since a card already authors its own metadata key as a class
(e.g. featured-sessions), repeating that key in every token was
redundant. rewriteToken() now also accepts a bare, prefix-less
token — [[enTitle]] rewrites to the same [[featured-sessions:3.
enTitle]] as [[featured-sessions.enTitle]] would. Both forms can
be mixed on the same card; a dotted token starting with a
different key is left untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[MWPW-201134] Support bare-token shorthand in card-c2 hydrator
Helix cache purges must target exact file paths (no wildcards), so
PRs into main now get a bot comment listing changed non-doc files
prefixed with a `<domain>` placeholder for quick find-and-replace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clean rename with no back-compat bridge (nothing published yet):

- event-libs/v1/c2/blocks/card-c2/ -> event-card/ (card-c2.js/.css ->
  event-card.js/.css; session-routing.js's .card-c2[data-mr-stream-id]
  selector updated)
- event-libs/v1/c2/blocks/slider/ -> event-carousel/ (slider.js/.css ->
  event-carousel.js/.css; its own .card-c2 track-matching selectors
  updated to .event-card)
- event-libs/v1/hydrate/card-c2.js -> event-card.js; hydrate.js's
  HYDRATORS registry key and import updated; hydrateCardC2 renamed to
  hydrateEventCard
- libs.js's EVENT_BLOCKS_C2 entries updated
- All tests, mocks, and docs (event-card/docs/*, event-carousel/docs/
  README.md, upcoming-sessions/docs/README.md) renamed/updated to match

Authors must now use event-card/event-carousel as the block name in DA;
the old card-c2/slider class names no longer resolve to anything.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consistent with the card-c2/slider -> event-card/event-carousel rename
in this PR. dark-card is still deliberately not named "dark" (Milo's
own reserved global dark-background class), same reasoning as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename card-c2 block to event-card, slider block to event-carousel
Add PR workflow to list changed code files for cache purging
Authors can now attach an image to each Upcoming/Featured Sessions item:
pick or create a DA folder via a new column browser, upload a local
image into it, and the resulting content.da.live URL is written to the
session's imageUrl meta field and included in the copied JSON output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…edupe auth module

- Fold da-media-controller.js's listFolder/uploadMedia into da-sheet-controller.js
  so every DA admin-API call shares one auth singleton instead of two
  independently-synced copies.
- Reject '.'/'..' path segments (folder-name input, uploaded filename) before
  they reach admin.da.live — URL dot-segment normalization could otherwise
  retarget a request outside the intended org/repo.
- encodeURIComponent the uploaded filename before splicing it into the request
  and content.da.live URLs.
- Guard ImagePickerModal's folder loads against out-of-order responses and
  clean up stale columns on a failed fetch.
- Fix the "Add folder" loading guard (was comparing against the wrong column
  index and never actually engaged).
- Block every modal-close path while an upload is in flight, so closing mid-
  upload can no longer force-close a picker reopened for a different session.
- Minor: drop a redundant root refetch, align a network-error message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aem-code-sync

aem-code-sync Bot commented Aug 13, 2026

Copy link
Copy Markdown

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch and validate page speed.
In case there are problems, just click a checkbox below to rerun the respective action.

  • Re-run all PSI checks
  • Re-run failed PSI checks
  • Re-sync branch
Commits

@qiyundai
qiyundai requested a review from sharmeebuilds August 13, 2026 19:37
…ased output

Author feedback on the first pass (folder picker + raw content.da.live URL +
JSON copy) drove four changes:

- Drop the DA folder-picker UI entirely. Every upload now lands in one fixed,
  dot-prefixed hidden folder the app owns, namespaced by event + timestamp —
  authors never choose or see the destination.
- content.da.live isn't a servable site asset. Uploads are now previewed and
  published via the Helix/AEM admin API (admin.hlx.page), and the resulting
  aem.live URL is what lands in imageUrl.
- Image field now renders on its own line instead of squeezed into the
  compact watchUrl/mrStreamId row.
- Replace "Copy Homepage JSON" with "Copy Link", mirroring Schedule Maker:
  the full session-entry payload is base64-encoded into the app's own URL
  hash, copied as a real hyperlink (text prefixed with the config type and
  timestamped), and clicking it deep-links straight back into that config's
  editor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qiyundai and others added 3 commits August 13, 2026 15:12
tools/da-apps/tier-1-event-configurator is just where this app's own
loader HTML lives — it isn't part of the site's Helix content mount, so
uploaded assets there never resolved to a servable preview/publish URL.
events/homepage-assets is under the site's actual published content tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root-caused against real request/response evidence: the multipart body
sent to admin.da.live was empty (a raw File passed to FormData.append
loses its bytes through the DA SDK's wrapped fetch — writeSheet never
hit this because its own body is always an already-materialized Blob),
and DA lowercases the filename segment on write, so previewAsset/
publishAsset were targeting a path that didn't match where the file
actually landed.

- uploadMedia now reads the File into a real Blob before appending it.
- uploadMedia parses DA's response body and returns its canonical
  stored path/URLs instead of assuming the request path was honored.
- uploadAndPublishMedia previews/publishes that canonical path, and
  prefers DA's own returned liveUrl over re-deriving one.
- buildMediaAssetPath lowercases up front too, so the path we log/assume
  matches what DA will actually store from the start.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
accept="image/jpeg" is just a picker hint, not enforcement, so
handleFileChange also validates the actual selected file (falling back
to extension when a file has no MIME type) and rejects anything else
before it's ever eligible for upload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The link is authored directly in the block's doc body now, not a
section-metadata row — drop the stale metadataKey field and update the
toast/hint copy to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nt-card

upcoming-sessions.js and a new featured-sessions block now consume the
Homepage configurator's link directly: decorate.js's tec-homepage auto-block
builder decodes the link's hash payload and builds the right block with
{ heading, entries } as a data attribute, replacing both the old JSON-in-
section-metadata authoring path and the hydrate/featured-sessions/session-code
classname mechanism. Featured Sessions cards are generated by reusing
event-card.js's own init() (still a generic, independently-used block) rather
than duplicating its media/body/session-routing logic. Images are now
auto-populated from the link payload on both surfaces; an entry with no image
is dropped by event-card.js's existing "no image, no card" rule. Adds a
"Section heading" field to the configurator UI, threaded through the link
payload since there's no authored row left to source it from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qiyundai and others added 4 commits August 13, 2026 16:43
Review findings from the previous commit's pivot:
- featured-sessions.js and upcoming-sessions.js built card titles/tracks/
  heading via createTag's string html argument (insertAdjacentHTML), which
  parses markup rather than treating it as text. That data now comes from a
  decoded link payload rather than hand-authored DOM/section-metadata, so an
  attacker-influenced value there would execute. Switched those call sites to
  set .textContent after creating the element.
- featured-sessions.js set the CTA href straight from the decoded entry's
  url with no scheme check; a card missing sessionId never gets
  session-routing.js's click interception (and its safeUrl re-check), so a
  javascript: URI would fire on a plain click. Now sanitized via the existing
  safeUrl() allowlist before ever becoming an href.
- Deduped the eventId-mismatch log guard that was copy-pasted between the
  sessions-guide and tec-homepage auto-block builders in decorate.js into one
  warnIfEventIdMismatch() helper.
- Fixed a wrong relative-import depth in featured-sessions.test.js that was
  silently failing to load the module under test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…icker for image uploads

Visual refresh: --tec-* custom properties in tier-1-event-configurator.css
now resolve to --s2a-* Spectrum S2 design tokens (imported from
v1/c2/styles/tokens.css) instead of hardcoded hex, matching the tokens
already established elsewhere in this repo (sessions-guide, c2 blocks, the
shared toast). Also: removed the 960px max-width cap on .tec-content
(fluid width), buttons are now fully pill-shaped, the "Section heading"
input is wider/more prominent, the editor back-button is vertically
centered against its two-line title block, the toast dropped its
non-Spectrum left-border stripe for solid variant backgrounds, and the
default browser 8px body margin is reset to 0.

Restored the DA folder-browser UI in ImagePickerModal.js (column browser,
breadcrumbs, "Add folder" pseudo-create-on-first-upload) that an earlier
commit on this branch had pivoted away from in favor of one fixed, hidden
upload folder. Kept everything from that later pivot that wasn't about
removing author choice: uploads still go through uploadAndPublishMedia
(upload, then preview, then publish, so the field ends up with a real
live aem.live URL) and JPG-only validation. utils.js's buildMediaAssetPath
(always-fixed path) is replaced by getDefaultMediaFolder(eventId) +
buildMediaFileName(fileName) — the picker defaults selectedFolderPath to
the event's own conventional folder, but authors can browse elsewhere or
type a new folder name to override it.

Security fix (from independent review): hasUnsafePathSegment in
v1/utils/da-sheet-controller.js only rejected literal '.'/'..' path
segments, which a percent-encoded equivalent (e.g. %2e%2e) could bypass
since neither listFolder nor uploadMedia decode the path before use. It
now decodes each segment (treating a malformed/undecodable segment as
unsafe) before comparing. The new folder-name input is now also
restricted client-side to [A-Za-z0-9._-], matching the character
allowlist already used elsewhere in this file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
featured-sessions.js was built as a plain wrapping grid (flex-wrap, no
controls) while its sibling upcoming-sessions.js — built in the same
commit, from the same tec-homepage auto-block builder — is a real
horizontally-scrolling carousel with arrow controls. That divergence
wasn't a deliberate design choice; every "Featured Sessions"-style
section on the team's own cards.plain.html demo doc renders as a
carousel, confirming this block should match.

Rather than duplicating event-carousel.js's arrow/scroll-state logic a
third time (event-carousel.js and upcoming-sessions.js already each have
their own copy), featured-sessions.js now reuses event-carousel.js
directly: it builds a .carousel-track holding the cards plus a bare
.event-carousel marker element, places them as siblings, and calls
event-carousel.js's own init() on the marker — the same selfInit
reasoning mobile-rider.js already uses inside event-marquee.js. Since
that bypasses Milo's normal per-block CSS auto-load (the marker is built
programmatically, not scanned from authored content), event-carousel.js's
init() now loads its own stylesheet first, matching mobile-rider.js's
existing precedent.

The heading is deliberately not routed through event-carousel.js's own
heading mechanism: that lives inside .carousel-controls, which is hidden
below 900px by design — fine for optional arrows, wrong for the block's
actual heading text (real page content, not a random encoding bug: it's
attacker-influenced, decoded from the link's hash payload, so it's set
via .textContent rather than createTag's HTML-argument path). The heading
instead gets its own always-visible element, and the now-decorated
marker is moved next to it afterward purely for layout — safe once
event-carousel.js's click handlers/ResizeObserver are already bound to
real elements, not DOM position.

Also removed hardcoded font-family declarations from event-card.css and
event-carousel.css's .carousel-pill so the site's global font rules
cascade through instead of being pinned locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ence

Saving a config redirected back to the library list immediately afterward
— present since the app's very first commit, not something recent work
introduced, but not useful now that saving gives its own toast
confirmation (ConfigsContext's saveActiveConfig already sets toastSuccess).
Authors editing a config likely want to keep working on it after a save,
not get bounced back to the list. Cancel still returns to the library, since
discarding and leaving is exactly what that button means.

MOBILE-RIDER-STREAM-ID-GAP.md described buildSessionAuthorEntry() as
building JSON pasted into section-metadata and listed a manual
mrStreamId/watchUrl input field as an undecided option — both stale: the
pre-link section-metadata authoring path no longer exists (superseded by
Copy Link), and that manual field was already implemented in
FeaturedSessionsEditor.js. Updated the doc to describe the actual current
flow and mark the decision resolved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sharmeebuilds
sharmeebuilds merged commit bc4cf73 into homepage-configurator Aug 14, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants