Skip to content

Commit 2820af0

Browse files
Copilothotlong
andcommitted
fix: expand kanban title fallback chain and improve board visuals
- Title resolution now tries: name → title → subject → label → display_name - Cards have left-border accent for visual hierarchy - Column headers use Badge component for count visibility - Empty columns show "No cards" placeholder - Board container has subtle background for visual separation - Added 10 unit tests for title fallback chain Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 3994186 commit 2820af0

3 files changed

Lines changed: 128 additions & 15 deletions

File tree

packages/plugin-kanban/src/KanbanImpl.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function SortableCard({ card, onCardClick, conditionalFormatting }: { card: Kanb
133133
<div ref={setNodeRef} style={style} {...attributes} {...listeners} role="listitem" aria-label={card.title}
134134
onClick={() => onCardClick?.(card)}
135135
>
136-
<Card className="mb-2 cursor-grab active:cursor-grabbing border-border bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group touch-manipulation" style={cardStyles}>
136+
<Card className="mb-2 cursor-grab active:cursor-grabbing border-border border-l-4 border-l-primary/40 bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group touch-manipulation" style={cardStyles}>
137137
{card.coverImage && (
138138
<div className="w-full h-32 overflow-hidden rounded-t-lg">
139139
<img
@@ -260,14 +260,14 @@ function KanbanColumnView({
260260
column.className
261261
)}
262262
>
263-
<div className="p-3 sm:p-4 border-b border-border/50 bg-muted/20">
263+
<div className="p-3 sm:p-4 border-b border-border/50 bg-muted/30 rounded-t-lg">
264264
<div className="flex items-center justify-between">
265265
<h3 id={`kanban-col-${column.id}`} className="font-mono text-xs sm:text-sm font-semibold tracking-wider text-primary/90 uppercase truncate">{column.title}</h3>
266266
<div className="flex items-center gap-2">
267-
<span className="font-mono text-xs text-muted-foreground" aria-label={`${safeCards.length} cards${column.limit ? ` of ${column.limit} maximum` : ''}`}>
267+
<Badge variant="secondary" className="text-xs font-mono tabular-nums">
268268
{safeCards.length}
269269
{column.limit && ` / ${column.limit}`}
270-
</span>
270+
</Badge>
271271
{isLimitExceeded && (
272272
<Badge variant="destructive" className="text-xs">
273273
Full
@@ -282,6 +282,11 @@ function KanbanColumnView({
282282
strategy={verticalListSortingStrategy}
283283
>
284284
<div className="space-y-2" role="list" aria-label={`${column.title} cards`}>
285+
{safeCards.length === 0 && (
286+
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground/50">
287+
<span className="text-xs font-mono">No cards</span>
288+
</div>
289+
)}
285290
{safeCards.map((card) => (
286291
<SortableCard key={card.id} card={card} onCardClick={onCardClick} conditionalFormatting={conditionalFormatting} />
287292
))}
@@ -567,7 +572,7 @@ function KanbanBoardInner({ columns, onCardMove, onCardClick, className, dnd, qu
567572
</div>
568573
) : (
569574
/* Standard flat layout */
570-
<div className={cn("flex gap-3 sm:gap-4 overflow-x-auto snap-x snap-mandatory p-2 sm:p-4 [-webkit-overflow-scrolling:touch]", className)} role="region" aria-label="Kanban board">
575+
<div className={cn("flex gap-3 sm:gap-4 overflow-x-auto snap-x snap-mandatory p-2 sm:p-4 bg-muted/10 rounded-lg [-webkit-overflow-scrolling:touch]", className)} role="region" aria-label="Kanban board">
571576
{boardColumns.map((column) => (
572577
<KanbanColumnView
573578
key={column.id}

packages/plugin-kanban/src/ObjectKanban.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,31 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
122122
}
123123
}
124124

125-
// Default to 'name'
126-
const finalTitleField = titleField || 'name';
127-
128-
return rawData.map(item => ({
129-
...item,
130-
// Ensure id exists
131-
id: item.id || item._id,
132-
// Map title
133-
title: item[finalTitleField] || item.title || 'Untitled',
134-
}));
125+
// Common title field names to try as fallback
126+
const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name'];
127+
128+
return rawData.map(item => {
129+
// If a specific title field was configured, try it first
130+
let resolvedTitle = titleField ? item[titleField] : undefined;
131+
132+
// Fallback: try common field names
133+
if (!resolvedTitle) {
134+
for (const field of TITLE_FALLBACK_FIELDS) {
135+
if (item[field]) {
136+
resolvedTitle = item[field];
137+
break;
138+
}
139+
}
140+
}
141+
142+
return {
143+
...item,
144+
// Ensure id exists
145+
id: item.id || item._id,
146+
// Map title
147+
title: resolvedTitle || 'Untitled',
148+
};
149+
});
135150
}, [rawData, schema, objectDef]);
136151

137152
// Generate columns if missing but groupBy is present
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi } from 'vitest';
10+
import { renderHook } from '@testing-library/react';
11+
import { useMemo } from 'react';
12+
13+
/**
14+
* Tests for the title resolution fallback chain in ObjectKanban.
15+
*
16+
* The effectiveData logic tries fields in this order:
17+
* 1. Explicit cardTitle / titleField from schema
18+
* 2. objectDef.titleFormat (e.g. "{subject}")
19+
* 3. objectDef.NAME_FIELD_KEY
20+
* 4. Fallback chain: name → title → subject → label → display_name
21+
* 5. 'Untitled'
22+
*/
23+
24+
// Extract the title resolution logic from ObjectKanban to test it in isolation
25+
const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name'];
26+
27+
function resolveTitle(item: Record<string, any>, titleField?: string): string {
28+
let resolvedTitle = titleField ? item[titleField] : undefined;
29+
30+
if (!resolvedTitle) {
31+
for (const field of TITLE_FALLBACK_FIELDS) {
32+
if (item[field]) {
33+
resolvedTitle = item[field];
34+
break;
35+
}
36+
}
37+
}
38+
39+
return resolvedTitle || 'Untitled';
40+
}
41+
42+
describe('ObjectKanban title resolution', () => {
43+
it('uses explicit titleField when value exists', () => {
44+
const item = { id: '1', custom_title: 'My Custom Title', name: 'Fallback Name' };
45+
expect(resolveTitle(item, 'custom_title')).toBe('My Custom Title');
46+
});
47+
48+
it('falls back to common fields when titleField value is empty', () => {
49+
const item = { id: '1', custom_title: '', name: 'Name Field' };
50+
expect(resolveTitle(item, 'custom_title')).toBe('Name Field');
51+
});
52+
53+
it('resolves name field first in fallback chain', () => {
54+
const item = { id: '1', name: 'Name Value', title: 'Title Value', subject: 'Subject Value' };
55+
expect(resolveTitle(item)).toBe('Name Value');
56+
});
57+
58+
it('resolves title field second in fallback chain', () => {
59+
const item = { id: '1', title: 'Title Value', subject: 'Subject Value' };
60+
expect(resolveTitle(item)).toBe('Title Value');
61+
});
62+
63+
it('resolves subject field third in fallback chain', () => {
64+
const item = { id: '1', subject: 'Subject Value', label: 'Label Value' };
65+
expect(resolveTitle(item)).toBe('Subject Value');
66+
});
67+
68+
it('resolves label field fourth in fallback chain', () => {
69+
const item = { id: '1', label: 'Label Value', display_name: 'Display Name' };
70+
expect(resolveTitle(item)).toBe('Label Value');
71+
});
72+
73+
it('resolves display_name field fifth in fallback chain', () => {
74+
const item = { id: '1', display_name: 'Display Name' };
75+
expect(resolveTitle(item)).toBe('Display Name');
76+
});
77+
78+
it('falls back to Untitled when no common fields exist', () => {
79+
const item = { id: '1', status: 'open', priority: 'high' };
80+
expect(resolveTitle(item)).toBe('Untitled');
81+
});
82+
83+
it('skips falsy field values in fallback chain', () => {
84+
const item = { id: '1', name: '', title: null, subject: 'Bug Report' };
85+
expect(resolveTitle(item)).toBe('Bug Report');
86+
});
87+
88+
it('handles todo_task objects with subject field', () => {
89+
// This is the exact scenario from the bug report
90+
const todoTask = { id: '1', status: 'in_progress', subject: 'Fix login bug', priority: 'high' };
91+
expect(resolveTitle(todoTask)).toBe('Fix login bug');
92+
});
93+
});

0 commit comments

Comments
 (0)