Document Tree: Fix undefined name for variants without fallback. (#21046)
* fix(backoffice): Tree menu item shows undefined for variant names without fallback When a document variant has no name set and there's no fallback language configured, the tree now falls back to the first variant with any name instead of displaying "undefined". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(backoffice): Show (Untitled) when no variant has a name 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbDocumentItemDataResolver } from './document-item-data-resolver.js';
|
||||
import { UmbVariantContext } from '@umbraco-cms/backoffice/variant';
|
||||
|
||||
// ============================================
|
||||
// SETUP: Create a test host element
|
||||
// ============================================
|
||||
// Controllers need a "host" element to attach to.
|
||||
// This creates a simple HTML element that can host controllers.
|
||||
@customElement('umb-test-controller-host')
|
||||
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
|
||||
|
||||
describe('UmbDocumentItemDataResolver', () => {
|
||||
let hostElement: UmbTestControllerHostElement;
|
||||
let resolver: UmbDocumentItemDataResolver<any>;
|
||||
let variantContext: UmbVariantContext;
|
||||
|
||||
// ============================================
|
||||
// beforeEach: Runs before EACH test
|
||||
// ============================================
|
||||
beforeEach(async () => {
|
||||
// 1. Create a host element
|
||||
hostElement = new UmbTestControllerHostElement();
|
||||
document.body.appendChild(hostElement);
|
||||
|
||||
// 2. Create and set up the variant context
|
||||
// This tells the resolver which culture to use
|
||||
variantContext = new UmbVariantContext(hostElement);
|
||||
await variantContext.setCulture('en-US');
|
||||
await variantContext.setFallbackCulture('en-US');
|
||||
await variantContext.setAppCulture('en-US');
|
||||
|
||||
// 3. Create the resolver (it will consume the context automatically)
|
||||
resolver = new UmbDocumentItemDataResolver(hostElement);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// afterEach: Cleanup after EACH test
|
||||
// ============================================
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Test Group: Public API
|
||||
// ============================================
|
||||
describe('Public API', () => {
|
||||
it('has a name observable', () => {
|
||||
expect(resolver).to.have.property('name');
|
||||
});
|
||||
|
||||
it('has setData method', () => {
|
||||
expect(resolver).to.have.property('setData').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has getData method', () => {
|
||||
expect(resolver).to.have.property('getData').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has getName method', () => {
|
||||
expect(resolver).to.have.property('getName').that.is.a('function');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Test Group: Name Fallback Behavior (THE BUG FIX)
|
||||
// ============================================
|
||||
describe('name fallback behavior', () => {
|
||||
it('should use current variant name when available', async () => {
|
||||
// ARRANGE: Create mock data with a name for current culture
|
||||
const mockData = {
|
||||
entityType: 'document',
|
||||
unique: 'test-123',
|
||||
documentType: { unique: 'dt-1', icon: 'icon-document', collection: null },
|
||||
isTrashed: false,
|
||||
variants: [{ culture: 'en-US', name: 'English Title', state: 'Published' }],
|
||||
};
|
||||
|
||||
// ACT: Set the data
|
||||
resolver.setData(mockData);
|
||||
|
||||
// ASSERT: Name should be the variant name
|
||||
const name = await resolver.getName();
|
||||
expect(name).to.equal('English Title');
|
||||
});
|
||||
|
||||
it('should fall back to fallback culture name in parentheses', async () => {
|
||||
// ARRANGE: Current culture (de-DE) has no name, fallback (en-US) has name
|
||||
await variantContext.setCulture('de-DE');
|
||||
await variantContext.setFallbackCulture('en-US');
|
||||
|
||||
const mockData = {
|
||||
entityType: 'document',
|
||||
unique: 'test-123',
|
||||
documentType: { unique: 'dt-1', icon: 'icon-document', collection: null },
|
||||
isTrashed: false,
|
||||
variants: [
|
||||
{ culture: 'en-US', name: 'English Title', state: 'Published' },
|
||||
{ culture: 'de-DE', name: undefined, state: 'NotCreated' },
|
||||
],
|
||||
};
|
||||
|
||||
// ACT
|
||||
resolver.setData(mockData);
|
||||
|
||||
// ASSERT: Should use fallback name in parentheses
|
||||
const name = await resolver.getName();
|
||||
expect(name).to.equal('(English Title)');
|
||||
});
|
||||
|
||||
it('should fall back to first variant with name when current and fallback have no name', async () => {
|
||||
// ARRANGE: This is THE BUG FIX test!
|
||||
// - Current culture (de-DE) has no name
|
||||
// - Fallback culture (es-ES) has no name
|
||||
// - But fr-FR has a name
|
||||
await variantContext.setCulture('de-DE');
|
||||
await variantContext.setFallbackCulture('es-ES');
|
||||
|
||||
const mockData = {
|
||||
entityType: 'document',
|
||||
unique: 'test-123',
|
||||
documentType: { unique: 'dt-1', icon: 'icon-document', collection: null },
|
||||
isTrashed: false,
|
||||
variants: [
|
||||
{ culture: 'de-DE', name: undefined, state: 'NotCreated' },
|
||||
{ culture: 'es-ES', name: undefined, state: 'NotCreated' },
|
||||
{ culture: 'fr-FR', name: 'Titre Français', state: 'Published' },
|
||||
],
|
||||
};
|
||||
|
||||
// ACT
|
||||
resolver.setData(mockData);
|
||||
|
||||
// ASSERT: Should find and use the French name (first with a value)
|
||||
const name = await resolver.getName();
|
||||
expect(name).to.equal('(Titre Français)');
|
||||
});
|
||||
|
||||
it('should return (Untitled) when no variants have names', async () => {
|
||||
// ARRANGE: No variant has a name
|
||||
const mockData = {
|
||||
entityType: 'document',
|
||||
unique: 'test-123',
|
||||
documentType: { unique: 'dt-1', icon: 'icon-document', collection: null },
|
||||
isTrashed: false,
|
||||
variants: [
|
||||
{ culture: 'en-US', name: undefined, state: 'NotCreated' },
|
||||
{ culture: 'de-DE', name: undefined, state: 'NotCreated' },
|
||||
],
|
||||
};
|
||||
|
||||
// ACT
|
||||
resolver.setData(mockData);
|
||||
|
||||
// ASSERT: Should show (Untitled) placeholder
|
||||
const name = await resolver.getName();
|
||||
expect(name).to.equal('(Untitled)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -229,18 +229,24 @@ export class UmbDocumentItemDataResolver<DocumentItemModel extends UmbDocumentIt
|
||||
|
||||
#setName() {
|
||||
const variant = this.#getCurrentVariant();
|
||||
if (variant) {
|
||||
if (variant?.name) {
|
||||
this.#name.setValue(variant.name);
|
||||
return;
|
||||
}
|
||||
|
||||
const variants = this.getData()?.variants;
|
||||
if (variants) {
|
||||
const fallbackName = findVariant(variants, this.#fallbackCulture!)?.name;
|
||||
this.#name.setValue(`(${fallbackName})`);
|
||||
} else {
|
||||
this.#name.setValue(undefined);
|
||||
// Try fallback culture first, then first variant with any name
|
||||
const fallbackName =
|
||||
findVariant(variants, this.#fallbackCulture!)?.name ?? variants.find((x) => x.name)?.name;
|
||||
|
||||
if (fallbackName) {
|
||||
this.#name.setValue(`(${fallbackName})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.#name.setValue('(Untitled)');
|
||||
}
|
||||
|
||||
#setIsDraft() {
|
||||
|
||||
Reference in New Issue
Block a user