Repository navigation
feat(ui): printer photos - #176
Merged
Merged
Conversation
printer-detail, printer-list and printer-maintenance were all xdescribe with bare scaffolds that could never have run: no providers, no resolver data. Each now has a configured TestBed and assertions covering what the component actually does - resolver handoff, the category-driven field enabling, the create-vs-update branch. Left as-is otherwise; this is setup repair, not new behavior.
FilamentImagesPanelComponent becomes the shared EntityImagesPanelComponent, driven by an EntityImageTarget rather than a FilamentService injection. The ID and its gateway travel in one object: binding them as two independent inputs would re-erase the generic, letting a printer ID pair with a material gateway. FilamentImageComponent becomes SignedImageComponent, with alt now required - it is the only place the subject reaches the accessibility tree, so a shared default would label every image on the page identically. Every behavioral test moved with the panel and still passes; the URL-shape assertions moved to filament-image-gateway.spec.ts, because a URL assertion against a generic panel would only be testing the test double.
…cceeded persistDefault and flushReorder ran as independent subscriptions started from a tap, so uploadStagedImages emitted before they landed. The caller navigates away on that emission, which destroys the panel and cancels them through takeUntilDestroyed - silently dropping the photo the user starred and the order they dragged. They are chained into the returned observable instead. A file the API refused with a 400 is also no longer re-posted by the parent Save button, which called uploadStagedImages directly and so bypassed the hidden Retry button. It is still reported, so a save that left it behind is not a clean success.
…refetch SAS signing is bucketed to six hours server-side so a re-signed URL is byte-identical, which is what lets the browser image cache hit. The avatar suppressed the exact URL that failed, so inside a bucket the refreshed value was the value it was suppressing and a transiently-failed photo stayed hidden until the bucket rolled. The store now exposes a generation bumped by each completed fetch, and suppression is scoped to it: one attempt per refetch, and a genuinely dead blob fails again and re-suppresses. Logout also clears the map directly rather than waiting for the store's next read, so the previous user's photos cannot survive a session that outlives the Auth0 redirect.
PrinterAvatarComponent reads thumbnailFor() from a computed, and thumbnailFor started the fetch inline - which sets phase and, on the sync paths, map. Angular rejects that with NG0600, the error propagated out of the cell template that held the avatar, and the print list rendered with no rows at all. The read now schedules the check on a microtask instead. Every stubbed-store test passed straight through this, so the avatar spec gains a suite that renders against the real store: it reproduces NG0600 without the fix.
The handset card view names the printer too, and was missed when the other print surfaces got avatars - which the print-list-cards doc figure would have shipped without one.
Adds three demo printer photos and points printers-summary.json at the machines
they actually show - a screenshot of a Snapmaker labelled "(Bambu Lab A1)" is
worse than no photo at all. printer-thumbnails.json resolves through the capture
harness rather than a blob host, so the images come from the repo.
The five affected targets gain imagesLoaded('app-printer-avatar'), which waits
for decode rather than for a DOM node the way imagesRendered does. first-print-form
is deliberately not gated: its avatars live in mat-option elements that only exist
while the select overlay is open, and a figure of an open dropdown is not what
that tutorial step shows.
The printer list's square thumbnail rendered letterboxed. Its ::ng-deep override tied on specificity with the component's own `.signed-image img` rule, so which one applied came down to stylesheet order - and the caller lost. A parent cannot win that race with CSS alone, so fit is an input bound to an inline style instead: contain for the carousel, cover for a fixed-size thumbnail.
Drops what a glance at the repo already answers: the module/service inventories, the environment file list, and the boilerplate TestBed and LoggingService snippets. 270 lines to 182. Adds four things that were learned the hard way: - NG0600 - a signal written during computed() evaluation throws out of the template that read it, so a lazily-fetching store getter takes down the whole subtree rather than logging a warning. - Stubbing every collaborator hid exactly that: a green suite over a print list that rendered zero rows in a real browser. - lint-staged does not format .html, so a template edit can silently flip a file to CRLF. - A component input beats a ::ng-deep override, which only ties on specificity. Also corrects the docs path: authored pages are src/content/docs/*.md, and src/app/documentation/generated/ is gitignored build output.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets users attach photos to their printers, and shows a printer's default photo everywhere a printer is named — so a multi-printer user can tell their machines apart at a glance.
Needs HoffmanEngineering/3d-print-log-api#114.
What's here
The editor. The filament images panel is generalized into
EntityImagesPanelComponent, parameterized by anEntityImageTarget— an ID and the gateway that understands it, travelling together so a printer ID can never be handed to a material's endpoints.FilamentImageComponentbecomesSignedImageComponent. Printer detail gets the panel, with the create flow sequenced so the new ID is written back onto the form before the uploads run — without that, retrying after a failed upload creates a second printer.The cross-cutting surfaces.
PrinterAvatarComponentrenders the default photo on the print list, print cards, the grouped view, print detail, the printer picker, maintenance and analytics. All seven read one per-user map throughPrinterThumbnailStorerather than adding a signed URL to five DTOs. The store's endpoint is authenticated-only, so an anonymous visitor gets an empty map and therefore no avatars — a structural guarantee rather than a guard repeated on seven surfaces.Upload failures are classified. The panel prechecks size and type before uploading, and separates a 400 (these bytes will never be accepted — no retry offered) from anything else (retry offered). The callers navigate on
allSucceededrather than on an empty failure array, so a permanent rejection cannot look like success.Three bugs found after the first pass
An adversarial review plus a real browser run turned up three things worth calling out, each now covered by a test that fails without the fix:
The avatar took the print list down.
thumbnailFor()started its fetch inline, which writes a signal — and the avatar reads it from acomputed(), where Angular forbids that (NG0600). The error propagated out of the table cell holding the avatar, so/printsrendered with no rows at all. Every test passed straight through it, because they all stubbed the store. The read now schedules the fetch on a microtask, and the avatar spec gained a suite that renders against the real store.Deferred writes were cancelled by the navigation they triggered.
uploadStagedImagesreported success before the set-default and reorder requests landed; the caller then navigated away, destroying the panel and cancelling them throughtakeUntilDestroyed. The photo a user starred while it was still staged was silently dropped. Those writes are now chained into the returned observable.A failed thumbnail could never recover. SAS signing is bucketed to six hours server-side so a re-signed URL is byte-identical — deliberately, so the browser image cache can hit. The avatar suppressed the exact URL that failed, so inside a bucket the refreshed value was the value it was suppressing. The store now exposes a generation bumped by each completed fetch, and suppression is scoped to it: one attempt per refetch, and a genuinely dead blob re-suppresses.
Also fixed:
SignedImageComponentgained an explicitfitinput. The printer list's square thumbnail rendered letterboxed because its::ng-deepoverride tied on specificity with the component's own rule — a caller cannot win that race with CSS alone.Screenshots and docs
The home and docs captures are regenerated with printer photos, and three demo printer photos are committed. The fixture's makes and models were changed to match them — a screenshot of a Snapmaker labelled "(Bambu Lab A1)" is worse than no photo at all.
The five affected capture targets gain
imagesLoaded('app-printer-avatar'), which waits for decode rather than for a DOM node the wayimagesRendereddoes — a still-loading<img>already satisfies the old gate, which is the exact race that loses a photo from a capture.first-print-formis deliberately not gated: its avatars live inmat-options that only exist while the select overlay is open, and a figure of an open dropdown is not what that tutorial step shows.src/content/docs/printers.mdgains a Photos section, including that printer photos are never shown on a print's public page.Testing
npm run test:brief— 1748 passing.npm run lint:briefclean;npm run test:scripts415/415;node scripts/validate-docs.mjspasses.npx cypress run --spec cypress/e2e/prints/public-print-anonymous.cy.tsagainst a local API inE2ETesting— 3/3. It now asserts a logged-out visitor renders no avatar and issues no/api/Printers/thumbnailsrequest at all.One thing to flag
AGENTS.mdsays user-facing docs live insrc/documentation. That's stale — authored pages aresrc/content/docs/*.md, andsrc/app/documentation/generated/is build output. Not fixed here to keep the diff on-topic.