{"id":"6a53839f105c16659bbcc709","title":"2026 07 12","path":"how-to/committee/release-notes/2026-07-12","contentMarkdown":"# 12-Jul-2026 — Inbox &  Email Composer & Walks Admin Fixes [#310](https://github.com/nbarrett/ngx-ramblers/issues/310)\n\n## [build 762](https://github.com/nbarrett/ngx-ramblers/actions/runs/29191596251) — [commit eef7482](https://github.com/nbarrett/ngx-ramblers/commit/eef748250fd55f1ba07e81f39e3b46128c86c154)\n\n_____\n\n## Sent emails with an attachment are no longer silently dropped\n\nThe write-back that records a composer send onto its conversation stamped every attachment with an empty `contentType`, but the stored-message schema required that field. Mongoose rejects an empty string for a required field, so the message insert threw on any send carrying an attachment: the thread was created but the message was never stored, leaving an empty conversation and no \"Sent from Email Composer\" entry. Attachment-free sends stored fine, which is why it went unnoticed - 41 recorded sends, none with an attachment.\n\n- The write-back now uses the codebase's standard `application/octet-stream` fallback rather than an empty content type (`EmailAttachment` carries no content type to pass through).\n- The stored-attachment schema no longer requires `contentType`, `sizeBytes` or `s3Key`: a missing piece of attachment metadata must never discard the whole message. Inbound attachments still carry their real content types.\n\nEmails already dropped this way cannot be recovered - the send happened at the provider but nothing was stored locally; only new sends are fixed.\n\n## Attachment preview works across the www / non-www boundary\n\nPreviewing a CSV or text attachment does an in-page fetch of the file to render it, built from the group's configured address. When the page is served from `www.` but the group address is the bare domain, that fetch is cross-origin and the browser blocks it with a CORS error, while a plain Download is unaffected.\n\n- New `UrlService.sameOriginUrl(url)` rebuilds an `/api/...` URL onto the actual browser origin, so the preview read never crosses the www/non-www boundary. It rebuilds on `absoluteUrl()` rather than `baseUrl()`, which prefers the group href.\n- The attachment preview (CSV, text and the PDF iframe) now routes its URL through it.\n- Unit tests cover the rewrite, a relative path, and leaving non-API URLs untouched.\n\n## Send button available as soon as the email is sendable\n\nThe composer's Send button lived only inside the final Send step, so you had to click through the wizard to reach it.\n\n- A Send button now appears in the footer navigation on every step the moment the email is sendable (all required steps valid, no send blockers), and is hidden on the Send step itself, where the existing button remains.\n- It moves to the Send step and fires the same `confirmAndSend()` action, so progress and any confirmation stay visible.\n\n## Grouped-conversation badge removed\n\nThe stacked-icon count on grouped conversations read \"Grouped across N role inboxes\", which was confusing and, when the grouped threads share a single role, inaccurate. The folding behaviour is unchanged; only the badge and its now-unused helper are gone.\n\n## Self-serve Walks Manager CSV export from Event Data Management\n\nGroups migrating from other platforms hold walk data (like leader names) that never reached Walks Manager, so their imported walks arrive with blanks. Event Data Management now lets an admin export any category of events as Walks Manager format CSVs, fill in the gaps in a spreadsheet, and keep the ids needed to amend the same records later - the immediate driver being Walks Manager walks with no leader name.\n\n- Tick one or more category rows (event type + group code + input source) and Export N Selected Groups To CSV produces one correspondingly named file per row, e.g. `walks-export-<timestamp>-BK05-pvr-group-walk-file-import.csv`. Export is disabled until at least one row is ticked.\n- Include Walk IDs adds the `Walk ID` column as the matching key for a CSV amend round trip: the Ramblers id where the walk came from Walks Manager, falling back to the Mongo id for manually created and file-imported events.\n- Include Images emits a companion `...-images.csv` per group in exactly the format the CSV import consumes (`Walk ID`, `Image GUID`, `Local Filename`, `Image Order`), with the full CDN URL in `Image GUID` and the bare filename the import matcher looks for in `Local Filename`.\n- Everything arrives as a single download: a bare CSV when one file results, otherwise the files are bundled server-side by a new authenticated `POST /api/database/walks/csv-zip` endpoint (adm-zip)\n- no more browser multiple-download prompts.\n- Rows are built by the same `walkToWalkUploadRow` code as the Ramblers upload, so the output is genuine Walks Manager format; `walkUploadHeadings()` is now public with an opt-in Walk ID heading and the upload path is unchanged.\n\n### Fixes shaken out along the way\n\n- The CSV export component never reset its buffer between generateCsv() calls, so consecutive exports concatenated into one file with repeated header rows; the CSV assembly is now a pure `csvContentFrom()` function shared by the component and the new export.\n- `walkToWalkUploadRow` crashed on imported events with no difficulty (`Cannot read properties of undefined (reading 'description')`) and would have crashed next on a null description; both are now guarded.\n- Exports were silently truncated at the server's 1000-document unpaginated query cap; the export query now passes an explicit limit.\n- The events table now uses the shared `.ngx-data-table` / `.ngx-data-table-card` styling in place of the old granite `styled-table` look.\n- The Recreate Group Event Index button is gone end to end (page button, client service method, server route and controller)\n- superseded by data migrations.","contentHtml":"<h1>12-Jul-2026 — Inbox &amp;  Email Composer &amp; Walks Admin Fixes <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/310\">#310</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/29191596251\">build 762</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/eef748250fd55f1ba07e81f39e3b46128c86c154\">commit eef7482</a></h2>\n<hr>\n<h2>Sent emails with an attachment are no longer silently dropped</h2>\n<p>The write-back that records a composer send onto its conversation stamped every attachment with an empty <code>contentType</code>, but the stored-message schema required that field. Mongoose rejects an empty string for a required field, so the message insert threw on any send carrying an attachment: the thread was created but the message was never stored, leaving an empty conversation and no &quot;Sent from Email Composer&quot; entry. Attachment-free sends stored fine, which is why it went unnoticed - 41 recorded sends, none with an attachment.</p>\n<ul>\n<li>The write-back now uses the codebase&#39;s standard <code>application/octet-stream</code> fallback rather than an empty content type (<code>EmailAttachment</code> carries no content type to pass through).</li>\n<li>The stored-attachment schema no longer requires <code>contentType</code>, <code>sizeBytes</code> or <code>s3Key</code>: a missing piece of attachment metadata must never discard the whole message. Inbound attachments still carry their real content types.</li>\n</ul>\n<p>Emails already dropped this way cannot be recovered - the send happened at the provider but nothing was stored locally; only new sends are fixed.</p>\n<h2>Attachment preview works across the www / non-www boundary</h2>\n<p>Previewing a CSV or text attachment does an in-page fetch of the file to render it, built from the group&#39;s configured address. When the page is served from <code>www.</code> but the group address is the bare domain, that fetch is cross-origin and the browser blocks it with a CORS error, while a plain Download is unaffected.</p>\n<ul>\n<li>New <code>UrlService.sameOriginUrl(url)</code> rebuilds an <code>/api/...</code> URL onto the actual browser origin, so the preview read never crosses the www/non-www boundary. It rebuilds on <code>absoluteUrl()</code> rather than <code>baseUrl()</code>, which prefers the group href.</li>\n<li>The attachment preview (CSV, text and the PDF iframe) now routes its URL through it.</li>\n<li>Unit tests cover the rewrite, a relative path, and leaving non-API URLs untouched.</li>\n</ul>\n<h2>Send button available as soon as the email is sendable</h2>\n<p>The composer&#39;s Send button lived only inside the final Send step, so you had to click through the wizard to reach it.</p>\n<ul>\n<li>A Send button now appears in the footer navigation on every step the moment the email is sendable (all required steps valid, no send blockers), and is hidden on the Send step itself, where the existing button remains.</li>\n<li>It moves to the Send step and fires the same <code>confirmAndSend()</code> action, so progress and any confirmation stay visible.</li>\n</ul>\n<h2>Grouped-conversation badge removed</h2>\n<p>The stacked-icon count on grouped conversations read &quot;Grouped across N role inboxes&quot;, which was confusing and, when the grouped threads share a single role, inaccurate. The folding behaviour is unchanged; only the badge and its now-unused helper are gone.</p>\n<h2>Self-serve Walks Manager CSV export from Event Data Management</h2>\n<p>Groups migrating from other platforms hold walk data (like leader names) that never reached Walks Manager, so their imported walks arrive with blanks. Event Data Management now lets an admin export any category of events as Walks Manager format CSVs, fill in the gaps in a spreadsheet, and keep the ids needed to amend the same records later - the immediate driver being Walks Manager walks with no leader name.</p>\n<ul>\n<li>Tick one or more category rows (event type + group code + input source) and Export N Selected Groups To CSV produces one correspondingly named file per row, e.g. <code>walks-export-&lt;timestamp&gt;-BK05-pvr-group-walk-file-import.csv</code>. Export is disabled until at least one row is ticked.</li>\n<li>Include Walk IDs adds the <code>Walk ID</code> column as the matching key for a CSV amend round trip: the Ramblers id where the walk came from Walks Manager, falling back to the Mongo id for manually created and file-imported events.</li>\n<li>Include Images emits a companion <code>...-images.csv</code> per group in exactly the format the CSV import consumes (<code>Walk ID</code>, <code>Image GUID</code>, <code>Local Filename</code>, <code>Image Order</code>), with the full CDN URL in <code>Image GUID</code> and the bare filename the import matcher looks for in <code>Local Filename</code>.</li>\n<li>Everything arrives as a single download: a bare CSV when one file results, otherwise the files are bundled server-side by a new authenticated <code>POST /api/database/walks/csv-zip</code> endpoint (adm-zip)</li>\n<li>no more browser multiple-download prompts.</li>\n<li>Rows are built by the same <code>walkToWalkUploadRow</code> code as the Ramblers upload, so the output is genuine Walks Manager format; <code>walkUploadHeadings()</code> is now public with an opt-in Walk ID heading and the upload path is unchanged.</li>\n</ul>\n<h3>Fixes shaken out along the way</h3>\n<ul>\n<li>The CSV export component never reset its buffer between generateCsv() calls, so consecutive exports concatenated into one file with repeated header rows; the CSV assembly is now a pure <code>csvContentFrom()</code> function shared by the component and the new export.</li>\n<li><code>walkToWalkUploadRow</code> crashed on imported events with no difficulty (<code>Cannot read properties of undefined (reading &#39;description&#39;)</code>) and would have crashed next on a null description; both are now guarded.</li>\n<li>Exports were silently truncated at the server&#39;s 1000-document unpaginated query cap; the export query now passes an explicit limit.</li>\n<li>The events table now uses the shared <code>.ngx-data-table</code> / <code>.ngx-data-table-card</code> styling in place of the old granite <code>styled-table</code> look.</li>\n<li>The Recreate Group Event Index button is gone end to end (page button, client service method, server route and controller)</li>\n<li>superseded by data migrations.</li>\n</ul>\n"}