Skip to content

feat(media): show where media is used#2225

Open
khoinguyenpham04 wants to merge 5 commits into
feature/media-libraryfrom
media-usage-admin-ui
Open

feat(media): show where media is used#2225
khoinguyenpham04 wants to merge 5 commits into
feature/media-libraryfrom
media-usage-admin-ui

Conversation

@khoinguyenpham04

@khoinguyenpham04 khoinguyenpham04 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds a coverage-aware Used in section to the media detail dialog for local assets. Editors can see linked content titles, collections, identifiers, locales, and trash status; open active references without losing unsaved metadata; and load additional results when usage spans multiple pages.

The panel handles loading, empty, redacted, incomplete-index, and retry states. The supporting compatibility work canonicalizes legacy local media references by storage key and ensures seeded content stores canonical media IDs, so existing projects can populate accurate usage information after upgrading.

This is a completed child PR for the media library project and targets feature/media-library. The umbrella PR remains draft at #2218 for Matt's final review.

Closes: N/A

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). No messages.po changes are included.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion — N/A; this project was directly approved in Matt's maintainer handoff and is tracked by umbrella PR feat(admin): media library project #2218.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

Verified locally in the media detail dialog at desktop and mobile widths, in light and dark themes.

  • pnpm typecheck
  • pnpm lint
  • Admin focused suite: 62 tests passed
  • Core focused suite: 69 tests passed
  • Final Used in component suite: 17 tests passed
  • pnpm lint:json | jq '.diagnostics | length'0

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks media-usage-admin-ui. Updated automatically when the playground redeploys.

@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d053c6e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground d053c6e Jul 24 2026, 01:23 PM

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,646 lines across 19 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Jul 24, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do d053c6e Jul 24 2026, 01:22 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache d053c6e Jul 24 2026, 01:23 PM

@emdashbot emdashbot 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.

This PR adds a useful, coverage-aware "Used in" panel to local media details and completes the back-compat story by canonicalizing legacy local media references off their storage key while seeding. I read the diff, the new UI/backend files, the media-usage handler/repository, and the API route changes, and checked against AGENTS.md conventions.

Approach: The change is the right one for the media-library project. It keeps usage reads admin-only, uses the existing projection tables, threads includeUsage only when requested, validates inputs through the shared schemas, and invalidates media-query caches on mutations. The only architecture concern is that the new canonicalization logic is a bit brittle: it fails an entire content source when one legacy storage key is missing, and it doesn't extend the storage-key repair to portable-text assets or repeater file sub-fields.

What I checked

  • Localization: all new user-facing strings go through Lingui.
  • RTL-safe Tailwind: the new JSX uses logical classes and dir="auto"/dir="ltr" attributes; no physical ml-*/text-left classes were introduced.
  • SQL safety: dynamic table/column identifiers are validated with validateIdentifier() and passed to sql.ref(); values are parameterized.
  • API envelope / auth: routes return ApiResult, use unwrapResult, require media:read, content:read_drafts, and admin scope for details, and redact counts for callers that can’t read drafts.
  • Locale filtering on content tables: snapshot/repair queries are by row id, so the per-row locale is implicit; no missing-locale bug.
  • Logged-out query counts: usage endpoints are auth-locked; no hot-path regression.
  • Changeset: present and user-facing.

Headline issues

  1. Repeater sub-field discovery and extraction only look for image sub-fields, so file fields inside repeaters are silently invisible to usage even though top-level file fields are tracked.
  2. The new storage-key canonicalizer aborts the whole content source on the first unresolvable storage key, dropping otherwise-valid references on the same entry.
  3. Portable-text image assets are not passed through the same storageKey repair path as direct image values.
  4. Tests assert Tailwind class names (font-mono, text-[0.9em], h-10, etc.), which tests the implementation detail rather than observable behavior.
  5. One new internal import omits the required .js extension.

I did not run the test suite or linters; the findings above are from static analysis only.


Findings

  • [needs fixing] packages/core/src/media/usage/extractor.ts:88

    The repeater walker only extracts image sub-fields, so file fields inside repeaters are silently invisible to the usage index even though top-level file fields are tracked (the MediaUsageReferenceType includes file_field). If an editor puts a file in a repeater, the "Used in" panel will never show that reference.

    function extractRepeaterOccurrences(
    	occurrences: ExtractedMediaUsageOccurrence[],
    	seen: Set<string>,
    	fieldSlug: string,
    	value: unknown,
    	subFields: readonly MediaUsageExtractionSubField[] | undefined,
    ): void {
    	if (!Array.isArray(value) || !Array.isArray(subFields)) return;
    
    	for (const [itemIndex, item] of value.entries()) {
    		if (!isRecord(item)) continue;
    
    		for (const subField of subFields) {
    			if (subField.type !== "image" && subField.type !== "file") continue;
    
    			addOccurrence(occurrences, seen, {
    				fieldSlug,
    				fieldPath: `${fieldSlug}[${itemIndex}].${subField.slug}`,
    				referenceType: subField.type === "image" ? "image_field" : "file_field",
    				value: item[subField.slug],
    				fallbackKind: subField.type === "image" ? "image" : null,
    			});
    		}
    	}
    }
    
  • [needs fixing] packages/core/src/media/usage/content-fields.ts:92

    This mirrors the extractor gap: repeater sub-field discovery only indexes image children, so file sub-fields in repeaters are never discovered for usage indexing in the first place. Discovery and extraction should accept both image and file sub-field types.

    	const subFields: MediaUsageExtractionSubField[] = [];
    	for (const subField of validation.subFields) {
    		if (!isRecord(subField) || (subField.type !== "image" && subField.type !== "file")) continue;
    		if (typeof subField.slug !== "string") continue;
    		validateIdentifier(subField.slug, "media usage repeater sub-field slug");
    		subFields.push({ slug: subField.slug, type: subField.type as "image" | "file" });
    	}
    
  • [needs fixing] packages/core/src/media/usage/content-snapshots.ts:231

    canonicalizeLocalMediaOccurrences fails the whole content source when any legacy storage key cannot be resolved. That drops every other valid reference on the same content row and marks the collection as failed/partial, instead of gracefully degrading. If one old reference points at a deleted file, entries that also reference still-existing media stop contributing to usage.

    Replace the hard failure with a partial mapping: keep resolvable occurrences and proceed with sourceCompleteness: "partial" for unresolvable ones.

    	const resolvedOccurrences = occurrences.map(({ storageKey, ...occurrence }) => {
    		if (occurrence.provider !== "local" || !storageKey) return occurrence;
    		const canonicalId = mediaIdByStorageKey.get(storageKey);
    		if (!canonicalId) return occurrence;
    		return { ...occurrence, mediaId: canonicalId, providerAssetId: canonicalId };
    	});
    
    	return {
    		success: true,
    		occurrences: resolvedOccurrences,
    	};
    

    Then update the callers to set sourceCompleteness to "partial" when any storageKey was present but not found, rather than returning LOCAL_MEDIA_NOT_FOUND.

  • [suggestion] packages/core/src/media/usage/extractor.ts:120-126

    The portable-text image path builds a MediaRef without reading storageKey from block.asset.meta, even though the direct image-field path reads value.meta.storageKey to repair legacy references. If a legacy rich-text block stored its local asset with a storage-key reference, this path won't canonicalize it.

    		addRefOccurrence(occurrences, seen, {
    			fieldSlug,
    			fieldPath: `${fieldSlug}[${blockIndex}].asset.${ref.key}`,
    			referenceType: "portable_text_image",
    			ref: buildMediaRef({
    				id: ref.id,
    				provider,
    				mimeType: normalizeMimeValue(block.asset.mimeType),
    				fallbackKind: "image",
    				storageKey: provider === "local" ? readStorageKey(block.asset.meta) : null,
    			}),
    		});
    
  • [suggestion] packages/admin/src/components/MediaDetailPanel.tsx:27

    This new sibling import omits the .js extension; AGENTS.md requires .js on internal ESM imports and the file already uses it for ./DialogError.js.

    import { MediaUsedIn } from "./MediaUsedIn.js";
    
  • [suggestion] packages/admin/tests/components/MediaUsedIn.test.tsx:150-157

    These assertions check exact Tailwind class names (font-mono, text-[0.9em], text-base). Per AGENTS.md, that tests the implementation detail straight back, not the behavior a user observes. Keep the visible-text, dir, href, and separator checks; drop the class-name assertions.

  • [suggestion] packages/admin/tests/components/MediaUsedIn.test.tsx:173-179

    Similar to the above test block, these lines assert that specific Tailwind utility classes (w-full, -mx-2, border, border-kumo-line, bg-kumo-control, items-center, h-10, w-10) are present. Testing class strings pins styling rather than behavior. Prefer asserting that the link is visible, spans its container, and contains the expected accessible content.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond overlap and removed review/needs-review No maintainer or bot review yet labels Jul 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 24, 2026
@khoinguyenpham04

Copy link
Copy Markdown
Collaborator Author

This PR adds a useful, coverage-aware "Used in" panel to local media details and completes the back-compat story by canonicalizing legacy local media references off their storage key while seeding. I read the diff, the new UI/backend files, the media-usage handler/repository, and the API route changes, and checked against AGENTS.md conventions.

Approach: The change is the right one for the media-library project. It keeps usage reads admin-only, uses the existing projection tables, threads includeUsage only when requested, validates inputs through the shared schemas, and invalidates media-query caches on mutations. The only architecture concern is that the new canonicalization logic is a bit brittle: it fails an entire content source when one legacy storage key is missing, and it doesn't extend the storage-key repair to portable-text assets or repeater file sub-fields.

What I checked

  • Localization: all new user-facing strings go through Lingui.
  • RTL-safe Tailwind: the new JSX uses logical classes and dir="auto"/dir="ltr" attributes; no physical ml-*/text-left classes were introduced.
  • SQL safety: dynamic table/column identifiers are validated with validateIdentifier() and passed to sql.ref(); values are parameterized.
  • API envelope / auth: routes return ApiResult, use unwrapResult, require media:read, content:read_drafts, and admin scope for details, and redact counts for callers that can’t read drafts.
  • Locale filtering on content tables: snapshot/repair queries are by row id, so the per-row locale is implicit; no missing-locale bug.
  • Logged-out query counts: usage endpoints are auth-locked; no hot-path regression.
  • Changeset: present and user-facing.

Headline issues

  1. Repeater sub-field discovery and extraction only look for image sub-fields, so file fields inside repeaters are silently invisible to usage even though top-level file fields are tracked.
  2. The new storage-key canonicalizer aborts the whole content source on the first unresolvable storage key, dropping otherwise-valid references on the same entry.
  3. Portable-text image assets are not passed through the same storageKey repair path as direct image values.
  4. Tests assert Tailwind class names (font-mono, text-[0.9em], h-10, etc.), which tests the implementation detail rather than observable behavior.
  5. One new internal import omits the required .js extension.

I did not run the test suite or linters; the findings above are from static analysis only.

Findings

  • [needs fixing] packages/core/src/media/usage/extractor.ts:88
    The repeater walker only extracts image sub-fields, so file fields inside repeaters are silently invisible to the usage index even though top-level file fields are tracked (the MediaUsageReferenceType includes file_field). If an editor puts a file in a repeater, the "Used in" panel will never show that reference.

    function extractRepeaterOccurrences(
    	occurrences: ExtractedMediaUsageOccurrence[],
    	seen: Set<string>,
    	fieldSlug: string,
    	value: unknown,
    	subFields: readonly MediaUsageExtractionSubField[] | undefined,
    ): void {
    	if (!Array.isArray(value) || !Array.isArray(subFields)) return;
    
    	for (const [itemIndex, item] of value.entries()) {
    		if (!isRecord(item)) continue;
    
    		for (const subField of subFields) {
    			if (subField.type !== "image" && subField.type !== "file") continue;
    
    			addOccurrence(occurrences, seen, {
    				fieldSlug,
    				fieldPath: `${fieldSlug}[${itemIndex}].${subField.slug}`,
    				referenceType: subField.type === "image" ? "image_field" : "file_field",
    				value: item[subField.slug],
    				fallbackKind: subField.type === "image" ? "image" : null,
    			});
    		}
    	}
    }
    
  • [needs fixing] packages/core/src/media/usage/content-fields.ts:92
    This mirrors the extractor gap: repeater sub-field discovery only indexes image children, so file sub-fields in repeaters are never discovered for usage indexing in the first place. Discovery and extraction should accept both image and file sub-field types.

    	const subFields: MediaUsageExtractionSubField[] = [];
    	for (const subField of validation.subFields) {
    		if (!isRecord(subField) || (subField.type !== "image" && subField.type !== "file")) continue;
    		if (typeof subField.slug !== "string") continue;
    		validateIdentifier(subField.slug, "media usage repeater sub-field slug");
    		subFields.push({ slug: subField.slug, type: subField.type as "image" | "file" });
    	}
    
  • [needs fixing] packages/core/src/media/usage/content-snapshots.ts:231
    canonicalizeLocalMediaOccurrences fails the whole content source when any legacy storage key cannot be resolved. That drops every other valid reference on the same content row and marks the collection as failed/partial, instead of gracefully degrading. If one old reference points at a deleted file, entries that also reference still-existing media stop contributing to usage.
    Replace the hard failure with a partial mapping: keep resolvable occurrences and proceed with sourceCompleteness: "partial" for unresolvable ones.

    	const resolvedOccurrences = occurrences.map(({ storageKey, ...occurrence }) => {
    		if (occurrence.provider !== "local" || !storageKey) return occurrence;
    		const canonicalId = mediaIdByStorageKey.get(storageKey);
    		if (!canonicalId) return occurrence;
    		return { ...occurrence, mediaId: canonicalId, providerAssetId: canonicalId };
    	});
    
    	return {
    		success: true,
    		occurrences: resolvedOccurrences,
    	};
    

    Then update the callers to set sourceCompleteness to "partial" when any storageKey was present but not found, rather than returning LOCAL_MEDIA_NOT_FOUND.

  • [suggestion] packages/core/src/media/usage/extractor.ts:120-126
    The portable-text image path builds a MediaRef without reading storageKey from block.asset.meta, even though the direct image-field path reads value.meta.storageKey to repair legacy references. If a legacy rich-text block stored its local asset with a storage-key reference, this path won't canonicalize it.

    		addRefOccurrence(occurrences, seen, {
    			fieldSlug,
    			fieldPath: `${fieldSlug}[${blockIndex}].asset.${ref.key}`,
    			referenceType: "portable_text_image",
    			ref: buildMediaRef({
    				id: ref.id,
    				provider,
    				mimeType: normalizeMimeValue(block.asset.mimeType),
    				fallbackKind: "image",
    				storageKey: provider === "local" ? readStorageKey(block.asset.meta) : null,
    			}),
    		});
    
  • [suggestion] packages/admin/src/components/MediaDetailPanel.tsx:27
    This new sibling import omits the .js extension; AGENTS.md requires .js on internal ESM imports and the file already uses it for ./DialogError.js.

    import { MediaUsedIn } from "./MediaUsedIn.js";
    
  • [suggestion] packages/admin/tests/components/MediaUsedIn.test.tsx:150-157
    These assertions check exact Tailwind class names (font-mono, text-[0.9em], text-base). Per AGENTS.md, that tests the implementation detail straight back, not the behavior a user observes. Keep the visible-text, dir, href, and separator checks; drop the class-name assertions.

  • [suggestion] packages/admin/tests/components/MediaUsedIn.test.tsx:173-179
    Similar to the above test block, these lines assert that specific Tailwind utility classes (w-full, -mx-2, border, border-kumo-line, bg-kumo-control, items-center, h-10, w-10) are present. Testing class strings pins styling rather than behavior. Prefer asserting that the link is visible, spans its container, and contains the expected accessible content.

Fixed the Portable Text compatibility path, .js import, and tests; repeater file fields remain out of scope, and unresolved storage keys still fail safely so the UI never shows guessed usage as trustworthy.

@khoinguyenpham04 khoinguyenpham04 added the bot:review Trigger an emdashbot code review on this PR label Jul 24, 2026

@emdashbot emdashbot 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.

Re-review of the media-library "Used in" feature.

Approach: The change is still the right one for the project. The new UI surfaces coverage-aware usage references, routes are guarded, counts are redacted for callers without draft/admin access, and the canonicalization path lets legacy storage-key references resolve after upgrade.

What changed since the last review

  • The MediaDetailPanel → MediaUsedIn import now includes the required .js extension.
  • Portable Text image assets now pass storageKey through the same repair path as direct image fields.
  • The MediaUsedIn tests no longer assert Tailwind class strings; they check visible text, dir, and link hrefs.
  • A new MediaDetailPanel.test.tsx was added with several class/style assertions.

What I checked

  • Localization / RTL: all new UI strings go through Lingui; logical Tailwind classes and dir attributes are used.
  • SQL safety: dynamic table/column identifiers are validated with validateIdentifier(); values are parameterized via Kysely.
  • API envelope / auth: usage endpoints require media:read, content:read_drafts, and the admin scope; summaries redact counts for unscoped callers.
  • Locale filtering on content tables: snapshot/repair queries read by row id or translation_group, so the per-row locale is implicit; no missing-locale bug.
  • Logged-out query counts: all new usage paths are auth-locked; no hot-path regression.
  • Changeset: present and user-facing.

Headline conclusion
Three real gaps from the prior review are still open, plus one new test-quality issue in the MediaDetailPanel suite:

  1. Repeater sub-field discovery and extraction only handle image children, so file fields inside repeaters are invisible to usage.
  2. Legacy storage-key canonicalization aborts an entire content source when any key is missing, dropping otherwise-valid references on the same row.
  3. MediaDetailPanel.test.tsx repeats the pattern the last review removed from MediaUsedIn.test.tsx: asserting exact Tailwind classes and inline styles.

The first two are completeness/correctness bugs; the hard-failure case is especially painful during upgrades because a single broken legacy reference blocks usage indexing for the whole content source. I did not run the test suite or linters; these findings are from static analysis only.


Findings

  • [needs fixing] packages/core/src/media/usage/extractor.ts:88

    The repeater walker still skips every sub-field except image, so file fields inside repeaters are invisible to usage indexing even though top-level file fields are tracked. MediaUsageReferenceType already includes file_field, so the extraction path should accept both sub-field types and emit the correct reference type.

    		for (const subField of subFields) {
    			if (subField.type !== "image" && subField.type !== "file") continue;
    
    			addOccurrence(occurrences, seen, {
    				fieldSlug,
    				fieldPath: `${fieldSlug}[${itemIndex}].${subField.slug}`,
    				referenceType: subField.type === "image" ? "image_field" : "file_field",
    				value: item[subField.slug],
    				fallbackKind: subField.type === "image" ? "image" : null,
    			});
    		}
    
  • [needs fixing] packages/core/src/media/usage/content-fields.ts:92

    Discovery mirrors the extractor gap: it only indexes image children from repeater validation, so file sub-fields in repeaters are never added to the extraction set in the first place. Discovery and extraction must both accept image and file sub-field types.

    	for (const subField of validation.subFields) {
    		if (!isRecord(subField) || (subField.type !== "image" && subField.type !== "file")) continue;
    		if (typeof subField.slug !== "string") continue;
    		validateIdentifier(subField.slug, "media usage repeater sub-field slug");
    		subFields.push({ slug: subField.slug, type: subField.type as "image" | "file" });
    	}
    
  • [needs fixing] packages/core/src/media/usage/content-snapshots.ts:234

    canonicalizeLocalMediaOccurrences fails the whole content source when any legacy storage key cannot be resolved. That drops every other valid reference on the same content row and marks the collection as failed/partial instead of degrading gracefully. A broken legacy reference should not block indexing of otherwise-valid ones.

    Replace the hard failure with a partial mapping: keep resolvable references, preserve unresolvable ones with a null mediaId, and expose a hasUnresolved flag so callers can set sourceCompleteness to "partial".

    	const missingStorageKeys = storageKeys.filter(
    		(storageKey) => !mediaIdByStorageKey.has(storageKey),
    	);
    
    	return {
    		success: true,
    		hasUnresolved: missingStorageKeys.length > 0,
    		occurrences: occurrences.map(({ storageKey, ...occurrence }) => {
    			if (occurrence.provider !== "local" || !storageKey) return occurrence;
    			const canonicalId = mediaIdByStorageKey.get(storageKey);
    			if (canonicalId) {
    				return { ...occurrence, mediaId: canonicalId, providerAssetId: canonicalId };
    			}
    			return { ...occurrence, mediaId: null };
    		}),
    	};
    

    Then update the callers in loadContentMediaUsageSnapshots to set source.sourceCompleteness = result.hasUnresolved ? "partial" : "complete" for each canonicalized source, and the existing "LOCAL_MEDIA_NOT_FOUND" return path can be removed.

  • [suggestion] packages/admin/tests/components/MediaDetailPanel.test.tsx:140

    This test asserts exact Tailwind classes and inline styles (grid-cols-1, md:grid-cols-2, min(88dvh, 48rem), md:p-8, etc.). Per AGENTS.md, that tests the implementation detail straight back rather than behavior a user observes. Keep the DOM/structure checks (file facts live in the preview column, both columns are present) and drop the class/style assertions. There are similar class-name assertions later in the file (e.g., the filename input check around line 285); prefer deleting those too.

    	it("renders the responsive two-column viewport-bounded dialog layout", async () => {
    		const screen = await renderPanel();
    		const dialog = screen.getByRole("dialog", { name: "Media Details" }).element();
    		const header = screen.getByTestId("media-detail-dialog-header").element();
    		const body = screen.getByTestId("media-detail-dialog-body").element();
    		const previewColumn = screen.getByTestId("media-detail-dialog-preview-column").element();
    		const detailsColumn = screen.getByTestId("media-detail-dialog-details-column").element();
    		const fileFacts = screen.getByTestId("media-detail-dialog-file-facts").element();
    		const footer = screen.getByTestId("media-detail-dialog-footer").element();
    
    		expect(previewColumn.contains(fileFacts)).toBe(true);
    		expect(detailsColumn.contains(fileFacts)).toBe(false);
    		expect(dialog).toBeVisible();
    		expect(header).toBeVisible();
    		expect(footer).toBeVisible();
    	});
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 24, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant