diff --git a/packages/plugin-kanban/src/KanbanImpl.tsx b/packages/plugin-kanban/src/KanbanImpl.tsx index 7a84a85441..d916fda786 100644 --- a/packages/plugin-kanban/src/KanbanImpl.tsx +++ b/packages/plugin-kanban/src/KanbanImpl.tsx @@ -133,7 +133,7 @@ function SortableCard({ card, onCardClick, conditionalFormatting }: { card: Kanb
onCardClick?.(card)} > - + {card.coverImage && (
-
+

{column.title}

- + {safeCards.length} {column.limit && ` / ${column.limit}`} - + {isLimitExceeded && ( Full @@ -282,6 +282,11 @@ function KanbanColumnView({ strategy={verticalListSortingStrategy} >
+ {safeCards.length === 0 && ( +
+ No cards +
+ )} {safeCards.map((card) => ( ))} @@ -567,7 +572,7 @@ function KanbanBoardInner({ columns, onCardMove, onCardClick, className, dnd, qu
) : ( /* Standard flat layout */ -
+
{boardColumns.map((column) => ( = ({ } } - // Default to 'name' - const finalTitleField = titleField || 'name'; - - return rawData.map(item => ({ - ...item, - // Ensure id exists - id: item.id || item._id, - // Map title - title: item[finalTitleField] || item.title || 'Untitled', - })); + // Common title field names to try as fallback + const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name']; + + return rawData.map(item => { + // If a specific title field was configured, try it first + let resolvedTitle = titleField ? item[titleField] : undefined; + + // Fallback: try common field names + if (!resolvedTitle) { + for (const field of TITLE_FALLBACK_FIELDS) { + if (item[field]) { + resolvedTitle = item[field]; + break; + } + } + } + + return { + ...item, + // Ensure id exists + id: item.id || item._id, + // Map title + title: resolvedTitle || 'Untitled', + }; + }); }, [rawData, schema, objectDef]); // Generate columns if missing but groupBy is present diff --git a/packages/plugin-kanban/src/__tests__/ObjectKanbanTitle.test.tsx b/packages/plugin-kanban/src/__tests__/ObjectKanbanTitle.test.tsx new file mode 100644 index 0000000000..dbb8328352 --- /dev/null +++ b/packages/plugin-kanban/src/__tests__/ObjectKanbanTitle.test.tsx @@ -0,0 +1,93 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useMemo } from 'react'; + +/** + * Tests for the title resolution fallback chain in ObjectKanban. + * + * The effectiveData logic tries fields in this order: + * 1. Explicit cardTitle / titleField from schema + * 2. objectDef.titleFormat (e.g. "{subject}") + * 3. objectDef.NAME_FIELD_KEY + * 4. Fallback chain: name → title → subject → label → display_name + * 5. 'Untitled' + */ + +// Extract the title resolution logic from ObjectKanban to test it in isolation +const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name']; + +function resolveTitle(item: Record, titleField?: string): string { + let resolvedTitle = titleField ? item[titleField] : undefined; + + if (!resolvedTitle) { + for (const field of TITLE_FALLBACK_FIELDS) { + if (item[field]) { + resolvedTitle = item[field]; + break; + } + } + } + + return resolvedTitle || 'Untitled'; +} + +describe('ObjectKanban title resolution', () => { + it('uses explicit titleField when value exists', () => { + const item = { id: '1', custom_title: 'My Custom Title', name: 'Fallback Name' }; + expect(resolveTitle(item, 'custom_title')).toBe('My Custom Title'); + }); + + it('falls back to common fields when titleField value is empty', () => { + const item = { id: '1', custom_title: '', name: 'Name Field' }; + expect(resolveTitle(item, 'custom_title')).toBe('Name Field'); + }); + + it('resolves name field first in fallback chain', () => { + const item = { id: '1', name: 'Name Value', title: 'Title Value', subject: 'Subject Value' }; + expect(resolveTitle(item)).toBe('Name Value'); + }); + + it('resolves title field second in fallback chain', () => { + const item = { id: '1', title: 'Title Value', subject: 'Subject Value' }; + expect(resolveTitle(item)).toBe('Title Value'); + }); + + it('resolves subject field third in fallback chain', () => { + const item = { id: '1', subject: 'Subject Value', label: 'Label Value' }; + expect(resolveTitle(item)).toBe('Subject Value'); + }); + + it('resolves label field fourth in fallback chain', () => { + const item = { id: '1', label: 'Label Value', display_name: 'Display Name' }; + expect(resolveTitle(item)).toBe('Label Value'); + }); + + it('resolves display_name field fifth in fallback chain', () => { + const item = { id: '1', display_name: 'Display Name' }; + expect(resolveTitle(item)).toBe('Display Name'); + }); + + it('falls back to Untitled when no common fields exist', () => { + const item = { id: '1', status: 'open', priority: 'high' }; + expect(resolveTitle(item)).toBe('Untitled'); + }); + + it('skips falsy field values in fallback chain', () => { + const item = { id: '1', name: '', title: null, subject: 'Bug Report' }; + expect(resolveTitle(item)).toBe('Bug Report'); + }); + + it('handles todo_task objects with subject field', () => { + // This is the exact scenario from the bug report + const todoTask = { id: '1', status: 'in_progress', subject: 'Fix login bug', priority: 'high' }; + expect(resolveTitle(todoTask)).toBe('Fix login bug'); + }); +});