Skip to content

Add listing attributes for display and filtering - #1683

Merged
stefan-burke merged 11 commits into
mainfrom
attrs
Jul 10, 2026
Merged

Add listing attributes for display and filtering#1683
stefan-burke merged 11 commits into
mainfrom
attrs

Conversation

@stefan-burke

@stefan-burke stefan-burke commented Jul 9, 2026

Copy link
Copy Markdown
Member

What changed

This adds a multiple-choice attributes system for listings.

Owners can now create listing attributes, add options to them, reorder both attributes and options, and choose the options that apply to each listing from the listing page. Attributes show on public listing cards (homepage, order gallery, and multi-listing ticket pages), single and multi-listing ticket page headers and rows, and admin listing pages can be filtered by selected attribute options.

Why

Some listings need simple labels such as level, format, audience, or location style. These should help people understand and sort listings, without changing how bookings work.

What this means for users

Operators can keep useful listing details in one reusable place instead of repeating them in listing descriptions. Visitors see those details on every public surface — the homepage card grid, the order gallery, and the ticket/booking page (whether single-listing or multi-listing). Booking capacity, pricing, checkout, and registration rules are unchanged.

Checks

  • deno task precommit

Summary by CodeRabbit

  • New Features
    • Added admin listing-attribute management (attributes/options CRUD, option/attribute ordering, listing assignment).
    • Added an Attributes tab on admin listing pages for owners to select attribute options.
    • Added public rendering of selected attributes on listing cards plus ticket and order flows, and added admin filtering by listing attributes.
    • Extended locale strings and admin navigation to include attributes.
  • Bug Fixes
    • Deleting a listing now also clears saved attribute selections.
    • Listing attribute option saving prunes invalid option ids.
  • Tests
    • Added/expanded backend and UI test coverage for attribute CRUD, filtering, rendering, and selection persistence.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces reusable listing attributes with encrypted persistence, admin CRUD and listing assignment, dashboard filtering, public rendering, shared admin abstractions, migrations, styling, and tests.

Changes

Listing Attributes Feature

Layer / File(s) Summary
Database schema and attribute persistence
src/shared/db/attributes.ts, src/shared/db/migrations/..., src/shared/db/listings.ts
Adds attribute, option, and listing-assignment tables with migrations, encrypted queries, ordering, deletion, selection, and listing cleanup.
Admin attribute management and listing assignment
src/features/admin/attributes.ts, src/features/admin/listing-page*.ts, src/ui/templates/admin/attributes.tsx, src/ui/templates/admin/nav.tsx, src/locales/en/*
Adds owner-only attribute and option CRUD, reordering, confirmation flows, listing assignment, navigation, and translations.
Shared listing-choice and reorder abstractions
src/features/admin/listing-choice-post.ts, src/features/admin/questions.ts, src/ui/templates/admin/listing-panel-frame.tsx, src/ui/templates/admin/questions.tsx, src/ui/templates/components/*
Adds reusable choice persistence, panel loading, checkbox, and reorder helpers, and applies them to questions and attribute selection.
Admin attribute filtering
src/shared/listing-attribute-filter.ts, src/features/admin/dashboard.ts, src/ui/templates/admin/dashboard.tsx, src/ui/templates/admin/listing-attribute-filters.ts
Adds query-driven attribute filters that combine with type filters across admin listing tables.
Public attribute rendering
src/features/public/*, src/ui/templates/public/*, src/ui/static/style.scss
Loads selected attributes and renders them on homepage cards, order cards, and ticket pages with supporting styles.
Validation and test utilities
test/lib/*, test/shared/*, test/ui/*, test/test-utils/*
Adds coverage for persistence, routes, deletion cleanup, filters, public rendering, migrations, and shared test fixtures.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant attributesRoutes
  participant attributesDB as shared/db/attributes.ts
  participant ListingPage
  Admin->>attributesRoutes: POST listing attribute selections
  attributesRoutes->>attributesDB: prune and persist option IDs
  attributesDB-->>attributesRoutes: saved assignments
  attributesRoutes-->>Admin: redirect with success flash
  Admin->>ListingPage: open Attributes tab
  ListingPage->>attributesDB: load all options and selected IDs
  attributesDB-->>ListingPage: attribute panel data
Loading
sequenceDiagram
  participant Visitor
  participant PublicPages
  participant attributesDB as shared/db/attributes.ts
  participant PublicTemplates
  Visitor->>PublicPages: request listing page
  PublicPages->>attributesDB: getSelectedAttributesForListings
  attributesDB-->>PublicPages: attributesByListing
  PublicPages->>PublicTemplates: render listing data with attributes
  PublicTemplates-->>Visitor: listing cards or ticket page with attributes
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the new listing attributes system and its display/filtering use cases.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch attrs

Comment @coderabbitai help to get the list of available commands.

@stefan-burke
stefan-burke marked this pull request as ready for review July 9, 2026 19:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/admin/attributes.ts`:
- Line 164: The attribute option activity logs in the add/edit/delete paths are
using attribute.id, which produces unreadable numeric entries instead of the
attribute name. Update the logActivity calls in the attribute option handlers to
use attributeNameFlat(attribute) consistently, matching the existing readable
logging used elsewhere in this file and the attribute create/update/delete
messages.
- Around line 288-299: The `moveAttributeHandler` flow is decrypting full
attribute/option payloads just to locate ids, which is unnecessary work. Update
`handle` in `moveAttributeHandler` and the `loadContext` /
`handleListingAttributesPost` paths to use a lightweight non-decrypting lookup
helper instead of `getAllAttributesWithOptions()` and
`getAttributeWithOptions()`. Reuse or introduce the lightweight id-only approach
suggested in `src/shared/db/attributes.ts` so `swapAttributeOrder`, `readIds`,
and related lookups operate on ids without decrypting option text.

In `@src/features/admin/dashboard.ts`:
- Around line 86-106: `loadListingAttributeFilterContext` is fetching/decrypting
attributes for every listing even though `adminDashboardPage` only filters
against `filterSource` (`activeListings`). Update the helper to build
`attributesByListing` from the `filterSource` ids instead of `listings` ids, and
keep `attributeFilters`/`selectedAttributeFiltersFromRequest` wired through the
existing `getSelectedAttributesForListings` and
`attributeFilterGroupsForListings` flow so only the listings that participate in
filtering are loaded.
- Line 139: The active listings derivation in adminDashboardPage is using native
Array.prototype.filter instead of the FP helper. Update the activeListings
calculation in dashboard.ts to use the curried filter from `#fp`, matching the
existing style used for the same logic in adminDashboardPage and keeping the
code consistent with the FP-style guidelines.

In `@src/shared/db/attributes.ts`:
- Around line 267-292: The current getSelectedAttributesForListings path
decrypts the same attribute/option separately for each listing because
selectedRowsForListing feeds groupAttributeRows per listing. Update the shared
flow in attributes.ts so decrypting happens once per unique attribute_id and
option_id, then rebuild each listing’s grouped result from those shared
decrypted objects without mixing options across listings. Keep the per-listing
filtering semantics intact when adjusting
selectedRowsForListing/groupAttributeRows, and verify the behavior with the
existing attributes tests.
- Around line 159-187: `getAllAttributesWithOptions` is doing expensive full
decryption even when callers only need ids, such as `moveAttributeHandler` and
`handleListingAttributesPost`’s `readIds`. Add lightweight id-only helpers in
`src/shared/db/attributes.ts` for ordered attribute ids and attribute option
ids, and update those callers to use them instead of
`getAllAttributesWithOptions`. Keep
`groupAttributeRows`/`getAttributeWithOptions` for the places that actually need
decrypted `name` and `text`.

In `@src/ui/static/style.scss`:
- Around line 238-258: The spacing in listing-attributes currently relies on
--space-xs, which is not defined in the checked-in token set, so the gap styles
may not apply. Update the listing-attributes rules in style.scss to either
define --space-xs in the shared tokens or replace it with an existing spacing
token already used elsewhere, and keep the selectors .listing-attributes and
.listing-attributes > div aligned with the intended compact layout.

In `@test/lib/server-attributes.test.ts`:
- Around line 359-378: The test name in server-attributes.test.ts suggests
duplicate option ids are being deduplicated, but the current payload only checks
invalid-id filtering. Update the repeated-options test around
postRepeatedOptions and getListingAttributeOptionIds so it submits an actual
duplicate option id (for example, repeat one of the valid attribute.options ids)
while still including an invalid id if needed, then assert the saved ids are
unique and only the valid options remain. This will exercise the
unique(optionIds) path in setListingAttributeOptions instead of only the pruning
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b00c9ce4-0696-4f10-8b54-041a72025404

📥 Commits

Reviewing files that changed from the base of the PR and between fa6ada1 and e10d743.

📒 Files selected for processing (41)
  • src/features/admin/attributes.ts
  • src/features/admin/dashboard.ts
  • src/features/admin/index.ts
  • src/features/admin/listing-choice-post.ts
  • src/features/admin/listing-page-data.ts
  • src/features/admin/listing-page.ts
  • src/features/admin/questions.ts
  • src/features/public/pages.ts
  • src/features/public/ticket-submit.ts
  • src/features/public/types.ts
  • src/locales/en/attributes.json
  • src/locales/en/entity-pages.json
  • src/locales/en/index.ts
  • src/locales/en/terms.json
  • src/shared/db/attributes.ts
  • src/shared/db/listings.ts
  • src/shared/db/migrations.ts
  • src/shared/db/migrations/2026-07-09_listing_attributes.ts
  • src/shared/db/migrations/schema.ts
  • src/shared/listing-attribute-filter.ts
  • src/ui/static/style.scss
  • src/ui/templates/admin/attributes.tsx
  • src/ui/templates/admin/dashboard.tsx
  • src/ui/templates/admin/listing-attribute-filters.ts
  • src/ui/templates/admin/listing-panel-frame.tsx
  • src/ui/templates/admin/nav.tsx
  • src/ui/templates/admin/questions.tsx
  • src/ui/templates/components/aggregate-sections.tsx
  • src/ui/templates/components/reorder-table.tsx
  • src/ui/templates/public/homepage.tsx
  • src/ui/templates/public/listing-attributes.ts
  • src/ui/templates/public/reservations.tsx
  • test/lib/db/migration-schema-guard.test.ts
  • test/lib/server-attributes.test.ts
  • test/lib/server-listings-filter.test.ts
  • test/shared/db/attributes.test.ts
  • test/shared/db/listings/delete.test.ts
  • test/shared/listing-attribute-filter.test.ts
  • test/test-utils.ts
  • test/test-utils/db-helpers/attributes.ts
  • test/ui/templates/admin/listing-attribute-filters.test.ts

Comment thread src/features/admin/attributes.ts Outdated
Comment thread src/features/admin/attributes.ts
Comment thread src/features/admin/dashboard.ts
Comment thread src/features/admin/dashboard.ts Outdated
Comment thread src/shared/db/attributes.ts
Comment thread src/shared/db/attributes.ts
Comment thread src/ui/static/style.scss
Comment thread test/lib/server-attributes.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e7650e10e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1820 to +1823
listingAttributes={
singleListing
? attributesByListing.get(singleListing.id)
: undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep attributes visible on multi-listing ticket pages

When a customer opens a combined booking page such as /ticket/a+b or arrives from the cart with more than one listing, singleListing is null, so this passes undefined and the fetched attributesByListing is never rendered anywhere else in the listing rows. That makes public attributes disappear on multi-listing ticket pages even though renderCtx loaded them for every page listing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86e812c. The multi-listing ticket page now renders each listing's selected attributes on its own row — renderListingRow takes a pre-rendered attributesHtml string threaded from attributesByListing, so every row shows its own attributes the same way the single-listing header already did. Regression test in server-listings-filter.test.ts: "shows each listing's attributes on a multi-listing ticket page".

Comment on lines +217 to +221
await Promise.all([
buildTicketListingsWithGroupCapacity(listings),
buildDailyDateFilter(listings, requestedDate),
soldOutPackageIds(groups, requestedDate),
getSelectedAttributesForListings(listings.map((listing) => listing.id)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show attributes on order-gallery cards too

This only loads attributes for /listings; the public order catalog still renders listing cards through orderGalleryPage from src/features/public/order.ts without loading or passing attributesByListing, so when settings.orderEnabled is used as the storefront the same listing attributes remain invisible until after selection. Please thread these selected attributes through the order gallery card renderer as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86e812c. handleOrder now fetches attributesByListing via getSelectedAttributesForListings and threads it through orderGalleryPagerenderOrderCard, so each order card (selectable and unavailable) shows its selected attributes. Regression test in server-order.test.ts: "shows selected listing attributes on order cards".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shared/db/migrations/schema.ts (1)

872-894: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting the no-FK cleanup ownership, matching house style.

The new join table's comment explains the design rationale but, unlike group_listings, listing_parents, and modifier_listings, doesn't state which delete path is responsible for pruning orphaned rows once attributes/options/listings are removed (there are no FKs enforcing this).

📝 Suggested comment addition
   [
-    // A selected option on a listing. The option points to its attribute, so the
-    // listing does not need a separate attribute row: one path handles one or
-    // many selected options.
+    // A selected option on a listing. The option points to its attribute, so the
+    // listing does not need a separate attribute row: one path handles one or
+    // many selected options. No FKs (house style); listing/attribute/option
+    // deletion paths prune this table explicitly.
     "listing_attribute_options",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/migrations/schema.ts` around lines 872 - 894, Update the
comment for the listing_attribute_options table to document that it has no
foreign keys and identify the application delete/cleanup path responsible for
removing orphaned rows when listings, attributes, or options are deleted,
matching the wording and style used by group_listings, listing_parents, and
modifier_listings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/shared/db/migrations/schema.ts`:
- Around line 872-894: Update the comment for the listing_attribute_options
table to document that it has no foreign keys and identify the application
delete/cleanup path responsible for removing orphaned rows when listings,
attributes, or options are deleted, matching the wording and style used by
group_listings, listing_parents, and modifier_listings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f46257fe-b01b-4065-be57-d7fd15c53da6

📥 Commits

Reviewing files that changed from the base of the PR and between 8e7650e and ea76fbc.

📒 Files selected for processing (3)
  • src/shared/db/migrations.ts
  • src/shared/db/migrations/schema.ts
  • test/lib/db/migration-schema-guard.test.ts

@stefan-burke stefan-burke changed the title [codex] Add listing attributes for display and filtering Add listing attributes for display and filtering Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/ui/templates/public/reservations.tsx (2)

790-841: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Closed listings never show attributesHtml.

The isClosed branch (Lines 801-809) omits attributesHtml while the isSoldOut branch (Line 817) and the normal branch (Line 836) both render it. This diverges from order-gallery.tsx's unavailableCard, which renders attributesHtml for both closed and sold-out states. A listing with selected attributes will silently lose that info on ticket pages once it closes, but keep showing it on the /order gallery — an inconsistent, easy-to-miss UX regression.

🐛 Proposed fix
   if (isClosed) {
     return `
       <div class="ticket-row sold-out">
         ${imageHtml}
         <label>${escapeHtml(listing.name)}</label>
+        ${attributesHtml}
         <span class="sold-out-label">${t("public.registration_closed")}</span>
       </div>
     `;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/templates/public/reservations.tsx` around lines 790 - 841, Update the
isClosed branch of renderListingRow to render attributesHtml alongside the
listing name and closed-status label, matching the isSoldOut and normal branches
and the unavailableCard behavior in order-gallery.tsx.

1495-1567: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Package member rows still need attribute rendering. attributesByListing only reaches buildListingRows, so listings rendered through renderPackageControls/renderPackageSection never pass renderListingAttributes(...). Package-only listings will drop their selected attributes on public ticket pages; thread the attributes map through the package render path and render it in renderPackageMemberRow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/templates/public/reservations.tsx` around lines 1495 - 1567, Thread
opts.attributesByListing through renderPackageControls and renderPackageSection
into renderPackageMemberRow, and update each corresponding signature and call
site. In renderPackageMemberRow, use the listing’s attributes map to invoke
renderListingAttributes(...) so package member rows render selected attributes
consistently with buildListingRows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/lib/server-listings-filter.test.ts`:
- Around line 222-239: Strengthen the test “shows each listing's attributes on a
multi-listing ticket page” by assigning distinct attributes to both listing1 and
listing2, then use scoped assertions for each listing’s rendered section to
verify its own attribute name and option are present and the other listing’s
attribute is absent. Avoid relying only on response-wide text checks so
incorrect association or omission is detected.

---

Outside diff comments:
In `@src/ui/templates/public/reservations.tsx`:
- Around line 790-841: Update the isClosed branch of renderListingRow to render
attributesHtml alongside the listing name and closed-status label, matching the
isSoldOut and normal branches and the unavailableCard behavior in
order-gallery.tsx.
- Around line 1495-1567: Thread opts.attributesByListing through
renderPackageControls and renderPackageSection into renderPackageMemberRow, and
update each corresponding signature and call site. In renderPackageMemberRow,
use the listing’s attributes map to invoke renderListingAttributes(...) so
package member rows render selected attributes consistently with
buildListingRows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 92b08a89-9cfa-41ce-bdb5-caa61be79653

📥 Commits

Reviewing files that changed from the base of the PR and between ea76fbc and 86e812c.

📒 Files selected for processing (5)
  • src/features/public/order.ts
  • src/ui/templates/public/order-gallery.tsx
  • src/ui/templates/public/reservations.tsx
  • test/lib/server-listings-filter.test.ts
  • test/lib/server-order.test.ts

Comment thread test/lib/server-listings-filter.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86e812c677

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

opts.hideQuantity,
opts.prefill,
(info) => (memberIds.has(info.listing.id) ? undefined : opts.childCtx),
opts.attributesByListing ?? new Map(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Thread attributes into package member rows

On booking pages that contain packages, the package sections have already been rendered through renderPackageSection/renderPackageMemberRow before this standalone-row call receives attributesByListing, and those package helpers never receive the map. A listing sold only as a visible package member therefore still hides its selected attributes on /ticket/<package> or mixed package carts, even though renderCtx loaded attributes for every page listing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1fb040a. renderPackageMemberRow, renderPackageControls, and renderPackageSection now take attributesByListing and render each member's selected attributes via renderListingAttributes(attributesByListing.get(member.listing.id)), threaded from buildPageListingRows. Package member rows show their attributes the same way standalone rows do.

filters={filters}
listings={activeListings}
headerHtml={attributeFilterHtml}
listings={filterByAttribute(activeListings)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep CSV exports aligned with attribute-filtered listings

This makes the /admin/listings table respect the selected attribute filters, but the export link still points to /admin/listings/csv and handleListingsCsvExport only applies listingTypeFromRequest. When an owner filters the listing index by an attribute and clicks Export CSV, the downloaded file includes listings that are no longer in the table, so the export can be misleading; carry these attribute params through and apply the same filter server-side.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1fb040a. The CSV export handler now loads loadListingAttributeFilterContext and applies filterListingsByAttributes alongside the existing type filter, so the download respects the same attribute filters as the table. The export link in the listings index also carries the active attribute params via csvExportHref. Regression tests in server-listings-filter.test.ts: "CSV export link carries the active attribute filter" and "CSV export respects attribute filter".

Comment thread src/shared/db/attributes.ts Outdated
Comment on lines +348 to +350
export const getListingAttributeOptionIds = (
listingId: number,
): Promise<number[]> => listingAttributeOptions.getIds(listingId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the listingAttributeOptions alias export

AGENTS.md explicitly says not to export a name that is just another name for an existing method, and this wrapper adds no behavior over listingAttributeOptions.getIds (the adjacent setter follows the same pattern because setIds already dedupes). Keeping both names gives the same internal mechanism two APIs; internal callers should use listingAttributeOptions.getIds/setIds directly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1fb040a. Removed the getListingAttributeOptionIds alias export — callers (listing-page-data.ts and the tests) now use listingAttributeOptions.getIds directly, per the AGENTS.md guidance against alias exports. setListingAttributeOptions stays because it adds unique() dedup — a real transformation, not a pure alias.

hideQuantity = false,
prefill?: TicketPrefill,
childCtx?: ChildRenderCtx,
attributesHtml = "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render attributes for closed ticket rows

When a multi-listing booking page contains a listing whose registration is closed, the new attributesHtml value is passed into renderListingRow but the isClosed branch returns before rendering it, unlike sold-out and bookable rows. Fresh evidence for this narrower case is the newly added parameter still being skipped by the closed-row return, so closed listings lose their public attributes on /ticket/a+b.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1fb040a. The isClosed branch of renderListingRow now renders attributesHtml before the sold-out label, matching the sold-out and bookable branches. Closed listings keep their public attributes on multi-listing pages.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fb040ae0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/admin/dashboard.ts Outdated
const listings = filterListingsByType(type)(await loadSortedListings());
const csv = generateListingsCsv(listings, settings.timezone);
const { activeAttributeFilters, attributesByListing } =
await loadListingAttributeFilterContext(request, listings);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve attribute filters before narrowing CSV type

When /admin/listings/csv combines type= with an attribute option that exists on another listing type, this newly added load builds activeAttributeFilters from the already type-filtered listings, so selectedAttributeFiltersFromRequest treats that attribute param as invalid and drops it. For example, ?type=daily&attribute_1=<standard-only option> exports all daily listings instead of the empty intersection that the type+attribute filters describe; load the attribute filter context from the full listing set and then apply the type filter so valid attribute params are preserved.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecce9a2. loadListingAttributeFilterContext now loads from the full listing set (before filterListingsByType narrows it), then the type filter is applied after. So an attribute on a different listing type is still recognised by selectedAttributeFiltersFromRequest instead of being silently dropped.

Comment thread src/features/public/ticket-submit.ts Outdated
Comment on lines +369 to +371
getSelectedAttributesForListings(
ctx.listings.map((entry) => entry.listing.id),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include child listings when loading ticket attributes

When a parent listing renders selectable add-on/child listings on /ticket/<parent>, those child rows are built from ctx.childrenByParentId, but this new attributes load only requests ctx.listings. A selected attribute on the child listing is therefore unavailable to the render path and never appears beside the child option, even though the buyer is choosing that child on the ticket page; include the child ids in this load and thread them through the child block renderer.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecce9a2. The getSelectedAttributesForListings call in renderCtx now includes child listing ids (from ctx.childrenByParentIdchildren), so attributes on add-on/child listings are loaded and available to the render path alongside the parent listings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0287ad58bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +369 to +372
getSelectedAttributesForListings([
...ctx.listings.map((entry) => entry.listing.id),
...children.map((child) => child.id),
]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render child listing attributes in add-on options

Fresh evidence after the previous thread: this now loads child IDs, but the returned attributesByListing is only passed to ticketPage for listing/package rows; ChildRenderCtx still has no attributes map and renderChildOption/renderSoleChildOption render only the child name/price. For /ticket/<parent> with a selectable add-on child, the child's selected attributes therefore remain invisible despite being fetched; thread this map into the child renderer and render it beside each child option.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 64ed02b. attributesByListing is now threaded into ChildRenderCtx and renderChildOption/renderSoleChildOption render each child's selected attributes via renderListingAttributes(attributesByListing.get(child.listing.id)). Child add-on options show their attributes on parent ticket pages.

Comment on lines +74 to +78
export const listingAttributeOptions = linkTableSide(
"listing_attribute_options",
"listing_id",
"option_id",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve attributes when duplicating listings

This new per-listing link table is only written from the Attributes tab; the single-listing duplicate path (buildDuplicateListingInput plus the create afterWrite) and group duplicate path copy memberships, prices, and child edges but never read listingAttributeOptions.getIds(sourceId) or insert those option ids for the clone. In the normal workflow of duplicating a listing or group for another date, the public attribute labels disappear from the new listings and attribute filters no longer find them, even though the rest of the listing settings are preserved.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 64ed02b. Added copyListingAttributeOptionsTx which copies listing_attribute_options rows from source to clone within the same transaction as the other duplicate copies. Called from both the single-listing duplicate afterWrite (writeCreateListingGroups) and the group duplicate batch path (bulk-actions.ts). Regression test in server-attributes.test.ts: "copies attribute selections onto the duplicate".

@stefan-burke
stefan-burke enabled auto-merge July 10, 2026 08:11
@stefan-burke
stefan-burke disabled auto-merge July 10, 2026 08:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64ed02b771

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

child,
childDatesById,
)}>${label}</p>${priceHtml}`;
)}>${label}</p>${priceHtml}${attributesHtml}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide attributes for hidden sole children

When the only bookable child is hidden, renderSoleChildOption already suppresses the child name and price (visible ? ... : ""), and the existing render-selector test documents that nothing visible should identify that child. Appending attributesHtml unconditionally here leaks any selected attribute names/options for that hidden child on the public ticket page, so a hidden add-on with attributes can still be exposed to buyers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this slipped through the merge. #1683 was merged (as 09e63164) before this thread could be addressed, so the leak landed on main: renderSoleChildOption appended attributesHtml unconditionally after the suppressed label, leaking a hidden sole child’s attribute names/options to the public ticket page.

Fixed in follow-up #1708: attributesHtml is now gated on the same visible flag that already suppresses the name and price (reservations.tsx:665), so a hidden sole child keeps its data markers and pay-more price input (the fold/compat scripts need them) but shows nothing identifying.

Regression tests in render-selector.test.ts: a visible sole child shows its attributes (positive), and a hidden sole child does not leak them (the bug — fails before the fix, passes after).

The order gallery and multi-listing ticket pages loaded attributes for
every listing but never rendered them. Now each order card shows its
selected attributes, and each listing row on a multi-listing ticket page
shows its own attributes (the single-listing header already did).
…ows, CSV export filter, remove alias

- Thread attributesByListing through renderPackageMemberRow so package
  member rows show their selected attributes (same as standalone rows)
- Render attributesHtml in the isClosed branch of renderListingRow so
  closed listings still show their attributes on multi-listing pages
- Carry the active type + attribute filters through to the CSV export
  link and handler so the download stays aligned with the filtered table
- Remove getListingAttributeOptionIds alias export (per AGENTS.md —
  callers use listingAttributeOptions.getIds directly)
- Strengthen the multi-listing attribute test to assign distinct
  attributes to both listings
- Replace array .map/.filter/.flatMap and for...of with curried map,
  filter, flatMap, reduce, and pipe from #fp in listing-attribute-filter.ts
  and attributes.ts (AGENTS.md: prefer #fp over imperative loops)
- Inline col.generated/col.encrypted directly in attributeOptionsTable
  schema to eliminate the jscpd clone with questions/tables.ts
- Drop unused export on AttributeFilterOption type (no external caller)
…sting ids

- CSV export: load loadListingAttributeFilterContext from the full listing
  set (before filterListingsByType) so an attribute that only exists on a
  different listing type is still recognised rather than silently dropped
- Ticket page: include child listing ids (from childrenByParentId) in
  getSelectedAttributesForListings so attributes on add-on/child listings
  render on parent ticket pages
Add tests that pass attributesByListing with real data through the
single-package and multi-package+standalone rendering paths in
buildPageListingRows, covering the previously uncovered lines
(1537-1542, 1545-1552, 1572-1576) and their ?? branches.
… listing/group duplicate

- Thread attributesByListing into ChildRenderCtx and render child
  attributes in renderChildOption/renderSoleChildOption so add-on child
  listings show their attributes on parent ticket pages
- Add copyListingAttributeOptionsTx and call it from the single-listing
  duplicate afterWrite path so attribute selections are copied onto clones
- Copy attribute selections in the group duplicate batch path too
- Regression test: 'copies attribute selections onto the duplicate'
…d ?? branches

The three ?? new Map() fallbacks were dead code — ticketPage always
passes attributesByListing (defaulted to new Map() at the ticketPage
level), so the ?? right-hand side never fired. Making the property
required and destructuring it removes the uncovered branches.
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 10, 2026
Merged via the queue into main with commit 09e6316 Jul 10, 2026
1 check 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.

1 participant