Skip to content

feat(ui): unify content thumbnails into dot-content-thumbnail component (#36295)#36424

Draft
oidacra wants to merge 5 commits into
mainfrom
issue-36295-dot-content-thumbnail
Draft

feat(ui): unify content thumbnails into dot-content-thumbnail component (#36295)#36424
oidacra wants to merge 5 commits into
mainfrom
issue-36295-dot-content-thumbnail

Conversation

@oidacra

@oidacra oidacra commented Jul 3, 2026

Copy link
Copy Markdown
Member

Proposed Changes

  • Introduce dot-content-thumbnail in libs/ui: a single Angular presentational thumbnail viewer that replaces both the Stencil dot-contentlet-thumbnail web component and the Angular dot-temp-file-thumbnail across the admin app.
  • Architecture: viewer + per-type internal renderers (image/svg/pdf, playable/first-frame video, Material Symbols icon) with a loading state machine — PrimeNG-skeleton shimmer replica, 300ms cross-fade reveal, and icon fallback on media error. Exposes a stateChange output.
  • API: consumers pass the contentlet directly ([contentlet] + optional [options]) or a pre-resolved model ([thumbnail]). Mappers (contentletToThumbnailModel, tempFileToThumbnailModel) replicate the legacy Stencil URL strategies (/dA, /contentAsset, pdf_page, raw SVG asset).
  • Deliberate fixes over the Stencil behavior: SVG resolves before the fieldVariable path (never rasterized, renders object-contain, per the issue AC); image/* content types qualify as images when isImage is absent (image-editor saves); playable videos are never masked by the skeleton (the native element owns its loading UX); first-frame video previews use a #t=0.1 media fragment instead of the legacy canvas.
  • Icon glyphs keep the legacy Material mapping (pdfIcon -> insert_drive_file, etc.), auto-scale via container queries (iconSize input as exact override) and recolor through the --dot-content-thumbnail-icon-color CSS custom property (used by the UVE palette hover).
  • Migrated all 10 usages: file-field preview (temp + contentlet), asset-search card, browser-selector dataview, content-drive folder list, UVE palette contentlet, block-editor contentlet block + suggestions, relationship field list and select-existing-content dialog.
  • Deleted dot-temp-file-thumbnail (single consumer, absorbed) and marked the Stencil dot-contentlet-thumbnail as @deprecated (still used by the Stencil-internal dot-card-contentlet and legacy JSP pages).
  • Removed CUSTOM_ELEMENTS_SCHEMA from the 7 migrated components that only declared it for the web component, restoring template type-checking.
  • Fixed a pre-existing crash in the file-field preview info dialog when a temp file arrives without metadata (image-editor save) by synthesizing fallback metadata.

Scope note: this expands issue #36295 (originally file-field-only) to a full build-and-replace of every Angular usage, per team decision. The component was named dot-content-thumbnail instead of the issue's dot-file-thumbnail.

QA'd live against a running instance: all media types (raster image, SVG, PDF, video, icon fallback) across Binary/Image/File fields and all 10 migrated surfaces. Also QA'd in combination with #36406 (image editor on Image/File fields): the branches are independent and merge in any order.

This PR fixes: #36295

@mergify

mergify Bot commented Jul 3, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @oidacra's task in 37s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🤖 dotBot Review (Bedrock)

Reviewed 37 file(s); 12 candidate(s) → 8 confirmed, 2 uncertain (unverified, kept for review).

⚠️ Coverage capped: 0 file(s) + 4 lower-severity candidate(s) skipped (limits: 40 files, 12 candidates).

Confirmed findings

  • 🟠 High core-web/libs/block-editor/src/lib/shared/shared.module.ts:26 — Missing module export for DotContentThumbnailComponent
    DotContentThumbnailComponent is declared in SharedModule's declarations array but not included in exports, preventing its use in templates of other modules that import SharedModule. This would cause Angular template compilation errors when attempting to use the component.
  • 🟡 Medium core-web/libs/block-editor/src/lib/nodes/contentlet-block/contentlet-block.component.html:7 — Missing thumbnail dimensions in options
    The migrated dot-content-thumbnail component in contentlet-block.component.html line 7 omits the width and height parameters that were explicitly set to 50 in the original dot-contentlet-thumbnail usage. The PR description indicates the new component uses an [options] input that should include these dimensions for proper server-side resizing parameters and client-side layout stability. Without them, thumbnails may render at unexpected sizes or with missing resizing directives in their URLs.
  • 🟡 Medium core-web/libs/block-editor/src/lib/shared/components/suggestion-list/components/suggestions-list-item/suggestions-list-item.component.html:6 — Missing thumbnail dimensions may cause layout issues
    The removal of explicit [width] and [height] inputs (previously set to 42) without providing equivalent sizing via [options] input or CSS constraints could lead to inconsistent thumbnail dimensions in the suggestions list UI. The new dot-content-thumbnail component's current usage in this location shows no dimensional constraints being applied, which may break layout assumptions from the previous implementation.
  • 🟡 Medium core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field-preview/dot-file-field-preview.component.scss:40 — Fixed aspect ratio may cause unintended letterboxing
    The added aspect-ratio: 16/9 creates a fixed container proportion that forces all thumbnails into 16:9 frames. While the thumbnail component uses object-contain to preserve asset proportions, this parent constraint will create letterboxing (empty space) for assets with different aspect ratios (e.g. square images), unlike other thumbnail usages that likely adapt to native asset proportions.
  • 🟡 Medium core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field-preview/dot-file-field-preview.component.scss:63 — Fixed aspect-ratio forces square format for non-image content
    The CSS rule aspect-ratio: 1 on .code-card/.content-card containers forces square format regardless of content type. This conflicts with PR's goal of unified thumbnail handling, as PDF/text content thumbnails often use rectangular formats (e.g. Material's insert_drive_file icon is 1:1.3 ratio) which would appear compressed in 1:1 containers.
  • 🟡 Medium core-web/libs/portlets/edit-ema/ui/src/lib/palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.spec.ts:121 — Test does not verify thumbnail input binding
    The test checks the component's internal $thumbnail() signal but does not verify that the element's [thumbnail] input is correctly bound. This omission leaves a gap in template validation, as a broken binding (e.g., using $thumbnail instead of $thumbnail()) would not be detected.
  • 🟡 Medium core-web/libs/ui/src/lib/components/dot-asset-search/components/dot-asset-card/dot-asset-card.component.scss:37 — Thumbnail media may not fill container
    The CSS targets dot-content-thumbnail img/video but omits width/height declarations while using object-fit:contain. Parent container has width/height:100% but media elements need explicit dimensions to fill available space. Without width:100%/height:100%, media may render at intrinsic size leaving empty space in the 1:1 aspect ratio container.
  • 🟡 Medium core-web/libs/ui/src/lib/components/dot-content-thumbnail/components/dot-content-thumbnail-icon.component.ts:17 — Empty aria-label harms accessibility
    The component binds aria-label directly to $alt() without a default. If $alt() is undefined or empty, the aria-label becomes empty or missing, failing accessibility requirements. Screen readers require non-empty labels to announce icons properly. Fix: Add a default alt value (e.g., 'Content thumbnail icon') using input('default') or similar.

🔎 Uncertain (could not confirm or disprove — review manually)

  • 🟡 Medium core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field-preview/dot-file-field-preview.component.scss:206 — Potential image presentation regression due to removed object-fit: cover
    The removal of object-fit: cover in dot-file-field-preview.component.scss line 206 without visible replacement in the new component could lead to image display issues. While the PR description states SVG handling uses object-contain, there's no evidence from tool results that the new dot-content-thumbnail component handles other image types' aspect ratios appropriately. The grep search for the component came up empty, preventing verification of its styling implementation.
  • 🟡 Medium core-web/libs/portlets/edit-ema/ui/src/lib/palette/components/dot-uve-palette-contentlet/dot-uve-palette-contentlet.component.html:12 — Potential thumbnail sizing regression
    The removal of 'h-full w-full object-cover' classes may impact layout, but cannot confirm without viewing dot-content-thumbnail component's internal styling. Assumption: New component might handle sizing internally. Requires CSS inspection to verify presence of equivalent width/height/object-fit styles.

us.deepseek.r1-v1:0 · Run: #28683006674 · tokens: in: 161415 · out: 49151 · total: 210566 · calls: 67 · est. ~$0.483

hasTitleImage: true
} as unknown as DotCMSContentlet;

it('resolves the thumbnail model from a contentlet', () => {

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.

🔴 [Critical] Incorrect URL path construction for title images

The test expects that when hasTitleImage is true, the generated URL does not include the 'titleImage' segment. However, the component's logic in dot-content-thumbnail.component.ts shows that hasTitleImage: true should prepend 'titleImage' to the path. The test's expectation is inverted, indicating either a test error or a component bug. Since the test passes with this incorrect expectation, the component may be generating wrong URLs for title images.

constructor() {
effect(() => this.stateChange.emit(this.$state()));
}

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.

🔴 [Critical] Infinite reload loop on media error

The onMediaError handler sets the state to 'error', but subsequent ngOnChanges triggers loadMedia() again when inputs remain unchanged. This creates an infinite error/reload cycle as failed media attempts repeatedly re-trigger loading without input change validation.

<div class="h-14 w-20 shrink-0 overflow-hidden rounded-md bg-gray-100">
<dot-contentlet-thumbnail
[iconSize]="'24px'"
<dot-content-thumbnail

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.

🟠 [High] Missing required options in dot-content-thumbnail

The original dot-contentlet-thumbnail had iconSize="24px" and [playableVideo]="true". The replacement dot-content-thumbnail requires these settings via [options] input based on component API documentation in the PR description. Their absence may break icon sizing and video playback behavior in relationship field thumbnails.

@@ -26,7 +26,8 @@ import { SuggestionsService } from './services';
ReactiveFormsModule,
ButtonModule,
TagModule,

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.

🟠 [High] Incorrect Angular module configuration for DotContentThumbnailComponent

DotContentThumbnailComponent is imported directly in SharedModule's imports array. Angular modules should only import other modules, not individual components unless they are standalone. Since DotContentThumbnailComponent is not marked as standalone (based on its declaration in DotContentThumbnailModule), this configuration will cause Angular template errors. The correct approach is to import DotContentThumbnailModule which exports the component.

it('should render the unified thumbnail with the temp file pdf src', () => {
spectator.detectChanges();

expect(spectator.query(byTestId('contentlet-thumbnail'))).toBeTruthy();

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.

🟠 [High] Test uses legacy test ID after component migration

The test queries for '[data-testId="contentlet-thumbnail"]' but the new component uses 'dot-content-thumbnail' selector. This would cause test failures as the element cannot be found after migration to the new component.


const { inode, modDate } = NEW_FILE_MOCK.entity;

expect(spectator.query(byTestId('contentlet-thumbnail'))).toBeTruthy();

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.

🟠 [High] Test ID mismatch in contentlet thumbnail verification

The test checks for contentlet-thumbnail test ID but the new component uses dot-content-thumbnail without this ID. Test line 150 uses deprecated selector, causing false negatives.

`,
host: { class: 'flex h-full w-full items-center justify-center' },
changeDetection: ChangeDetectionStrategy.OnPush
})

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.

🟠 [High] Missing accessible name for icon fallback

The $alt input defaults to empty string, resulting in empty aria-label attributes. This violates WCAG 1.1.1 by failing to provide text alternatives for non-text content when alt isn't provided. Screen reader users receive no semantic information about the icon's purpose, as confirmed by the code binding aria-label directly to an empty default alt property.

* to the `dot-content-thumbnail` folder — consumers use the viewer.
*/
@Component({
selector: 'dot-content-thumbnail-image',

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.

🟠 [High] Cached image load events may fire before parent subscription

The image load event is triggered synchronously for cached images during ngOnInit, before parent components subscribe to stateChange. This results in the 'loaded' event being emitted but not captured, leaving thumbnails in a loading state indefinitely.

Introduce a single Angular presentational thumbnail viewer in libs/ui that
replaces both the Stencil dot-contentlet-thumbnail web component and the
Angular dot-temp-file-thumbnail across the whole admin app.

- New dot-content-thumbnail viewer with per-type internal renderers
  (image/svg/pdf via img, playable/first-frame video, Material Symbols icon),
  a loading state machine (PrimeNG-skeleton shimmer, 300ms cross-fade reveal)
  and icon fallback on media error.
- Consumers pass the contentlet directly ([contentlet] + [options]) or a
  pre-resolved model ([thumbnail]); mappers replicate the legacy Stencil URL
  strategies (/dA, /contentAsset, pdf_page, svg raw asset) with deliberate
  fixes: SVG resolves before the fieldVariable path (never rasterized, renders
  object-contain) and image/* content types qualify as images when isImage is
  absent (image-editor saves).
- Icon glyph auto-scales via container queries (iconSize input as override)
  and recolors through the --dot-content-thumbnail-icon-color CSS property.
- Playable videos are never masked by the skeleton: the native element owns
  its loading UX; first-frame previews use a #t=0.1 media fragment instead of
  the legacy canvas approach.
- Migrate all 10 usages: file-field preview (temp + contentlet), asset-search
  card, browser-selector dataview, content-drive folder list, UVE palette,
  block-editor contentlet block + suggestions, relationship field (list and
  select-existing dialog).
- Delete dot-temp-file-thumbnail (single consumer, absorbed) and mark the
  Stencil dot-contentlet-thumbnail as deprecated (kept for dot-card-contentlet
  and legacy JSP).
- Fix a pre-existing crash in the file-field preview info dialog when a temp
  file arrives without metadata (image-editor save) by synthesizing fallback
  metadata.

Refs: #36295
@oidacra oidacra force-pushed the issue-36295-dot-content-thumbnail branch from f32f938 to a212a70 Compare July 3, 2026 20:09
@oidacra oidacra changed the base branch from issue-36363-image-editor-image-file-fields to main July 3, 2026 20:09
@github-actions github-actions Bot added Area : Frontend PR changes Angular/TypeScript frontend code and removed AI: Safe To Rollback labels Jul 3, 2026
computed,
effect,
input,
linkedSignal,

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.

🔴 [Critical] Undefined linkedSignal function usage

The component imports and uses linkedSignal from '@angular/core' (line 7), but this function doesn't exist in Angular's public API. This will cause immediate runtime errors when the component initializes, as Angular cannot resolve the dependency.

DotContextMenuComponent,
DotAddButtonComponent,
DotContentletStatusBadgeComponent
DotContentletStatusBadgeComponent,

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.

🟠 [High] Angular module conflict

DotContentThumbnailComponent is added to both imports and declarations in BlockEditorModule. Angular requires components to be declared in exactly one module. Since it's imported from @dotcms/ui, it should only be in imports, not declarations. This duplication will cause Angular compiler errors.

TagModule,
DotContentletStatusBadgeComponent
DotContentletStatusBadgeComponent,
DotContentThumbnailComponent

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.

🟠 [High] Angular module configuration error

DotContentThumbnailComponent is declared in SharedModule's declarations array (core-web/libs/block-editor/src/lib/shared/shared.module.ts:30) despite being a standalone component. Standalone components should not be declared in NgModules - this will cause Angular template errors. The component should be removed from declarations array and instead imported directly in consuming components' imports.

template: `
<i
[style.font-size]="$size()"
[attr.aria-label]="$alt()"

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.

🟠 [High] Incorrect data-testid attribute casing

The attribute data-testId uses camelCase instead of the standard lowercase data-testid, which may cause test selectors relying on the correct casing to fail.

@oidacra oidacra marked this pull request as draft July 3, 2026 20:23
The component shipped styled with Tailwind utilities only, but it also
renders inside bundles that do not include Tailwind — the legacy binary
field web component (dotcms-binary-field-builder) — where the thumbnail
collapsed to a zero-height host and the whole preview card flattened.

Replace every Tailwind utility (host layout, media visibility/transition,
loading overlay, icon sizing) with component-scoped styles so the viewer
and its renderers carry their own layout in any bundle. Behavior, class
hooks for consumers (img/video selectors, --dot-content-thumbnail-icon-color)
and testids are unchanged; specs updated to the new class names.

Refs: #36295
it('should render the unified thumbnail with the temp file pdf src', () => {
spectator.detectChanges();

expect(spectator.query(byTestId('contentlet-thumbnail'))).toBeTruthy();

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.

🟠 [High] Incorrect test ID for content thumbnail component in test

The test uses '[data-testId="contentlet-thumbnail"]' but the new component's template uses 'dot-content-thumbnail' as the test ID. This mismatch causes the test to fail when it should pass, or pass when it should fail, leading to unreliable test results. The correct test ID should match the component's actual data-testId attribute.

}

@if ($hasLoadingOverlay()) {
<div

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.

🟠 [High] Incorrect data-testid attribute casing

The attribute data-testId in line 96 uses an uppercase 'I' instead of lowercase 'i', which is the standard for HTML data attributes. This typo breaks test selectors relying on data-testid, leading to failed tests.

…-height wrappers

container-type: size made the thumbnail contribute zero height, collapsing
any consumer whose wrapper derives its height from content — notably the
file field below its 500px container breakpoint (2-column Edit Content
layouts around 1496px viewport), where the whole preview card flattened
to a border line.

Switch the host to container-type: inline-size (no height containment) and
make the icon auto-size clamp use cqi. Since inline-size alone cannot fit
the glyph to short rows, restore the explicit legacy iconSize at every call
site (48px file field/dataview, 72px asset card/contentlet block, 42px
suggestions, 36px content drive, 32px UVE palette, 24/30px relationship);
the cqi clamp remains as a sane fallback for consumers that omit it.

Refs: #36295
…narrow layouts

Below the preview's 500px container breakpoint the card height derives
from the media's natural ratio, so a near-square video produced a much
taller card than a 16:9 image next to it. Give the thumbnail a 16:9
aspect-ratio — effective only on the narrow layout, since on the wide
one both axes are already definite and aspect-ratio is ignored.

Refs: #36295
…youts

Same narrow-layout issue as the media thumbnails: with the container at
height auto, the editableAsText code preview grew with its whole text
content. Apply the same 16:9 aspect-ratio cap (inert on the wide layout);
the existing bottom fade already signals the truncation.

Refs: #36295
@@ -26,7 +26,8 @@ import { SuggestionsService } from './services';
ReactiveFormsModule,

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.

🟠 [High] Missing module export for DotContentThumbnailComponent

DotContentThumbnailComponent is declared in SharedModule's declarations array but not included in exports, preventing its use in templates of other modules that import SharedModule. This would cause Angular template compilation errors when attempting to use the component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Unify file/binary/image field preview into a single Angular thumbnail (dot-file-thumbnail), drop Stencil dependency

1 participant