feat(enhanced-kanban): enhance section creation with category selection and input handling
- Added state management for section creation, including input focus and category selection. - Implemented dropdown for category selection with visual feedback and improved accessibility. - Refactored section creation logic to handle user input and category assignment more effectively. - Enhanced user experience by managing input focus and handling outside clicks to close dropdowns.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { nanoid } from '@reduxjs/toolkit';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||
@@ -33,6 +34,52 @@ const EnhancedKanbanCreateSection: React.FC = () => {
|
||||
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const isProjectManager = useIsProjectManager();
|
||||
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [sectionName, setSectionName] = useState('');
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string>('');
|
||||
const [showCategoryDropdown, setShowCategoryDropdown] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const categoryDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Find selected category object
|
||||
const selectedCategory = statusCategories?.find(cat => cat.id === selectedCategoryId);
|
||||
|
||||
// Compute header background color
|
||||
const headerBackgroundColor = React.useMemo(() => {
|
||||
if (!selectedCategory) return themeWiseColor('#f5f5f5', '#1e1e1e', themeMode);
|
||||
return selectedCategory.color_code || (themeMode === 'dark' ? '#1e1e1e' : '#f5f5f5');
|
||||
}, [themeMode, selectedCategory]);
|
||||
|
||||
// Focus input when adding
|
||||
useEffect(() => {
|
||||
if (isAdding && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isAdding]);
|
||||
|
||||
// Close on outside click (for both input and category dropdown)
|
||||
useEffect(() => {
|
||||
if (!isAdding && !showCategoryDropdown) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
inputRef.current &&
|
||||
!inputRef.current.contains(event.target as Node) &&
|
||||
(!categoryDropdownRef.current || !categoryDropdownRef.current.contains(event.target as Node))
|
||||
) {
|
||||
setIsAdding(false);
|
||||
setSectionName('');
|
||||
setSelectedCategoryId('');
|
||||
setShowCategoryDropdown(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isAdding, showCategoryDropdown]);
|
||||
|
||||
// Don't show for priority grouping or if user doesn't have permissions
|
||||
if (groupBy === IGroupBy.PRIORITY || (!isOwnerorAdmin && !isProjectManager)) {
|
||||
return null;
|
||||
@@ -59,49 +106,38 @@ const EnhancedKanbanCreateSection: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleAddSection = async () => {
|
||||
const sectionId = nanoid();
|
||||
const baseNameSection = 'Untitled section';
|
||||
const sectionName = getUniqueSectionName(baseNameSection);
|
||||
setIsAdding(true);
|
||||
setSectionName('');
|
||||
// Default to first category if available
|
||||
if (statusCategories && statusCategories.length > 0 && typeof statusCategories[0].id === 'string') {
|
||||
setSelectedCategoryId(statusCategories[0].id);
|
||||
} else {
|
||||
setSelectedCategoryId('');
|
||||
}
|
||||
};
|
||||
|
||||
if (groupBy === IGroupBy.STATUS && projectId) {
|
||||
// Find the "To do" category
|
||||
const todoCategory = statusCategories.find(
|
||||
category =>
|
||||
category.name?.toLowerCase() === 'to do' || category.name?.toLowerCase() === 'todo'
|
||||
);
|
||||
|
||||
if (todoCategory && todoCategory.id) {
|
||||
// Create a new status
|
||||
const body = {
|
||||
name: sectionName,
|
||||
project_id: projectId,
|
||||
category_id: todoCategory.id,
|
||||
};
|
||||
|
||||
try {
|
||||
// Create the status
|
||||
const response = await dispatch(
|
||||
createStatus({ body, currentProjectId: projectId })
|
||||
).unwrap();
|
||||
|
||||
if (response.done && response.body) {
|
||||
// Refresh the board to show the new section
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
// Refresh statuses
|
||||
dispatch(fetchStatuses(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to create status:', error);
|
||||
const handleCreateSection = async () => {
|
||||
if (!sectionName.trim() || !projectId) return;
|
||||
const name = getUniqueSectionName(sectionName.trim());
|
||||
if (groupBy === IGroupBy.STATUS && selectedCategoryId) {
|
||||
const body = {
|
||||
name,
|
||||
project_id: projectId,
|
||||
category_id: selectedCategoryId,
|
||||
};
|
||||
try {
|
||||
const response = await dispatch(
|
||||
createStatus({ body, currentProjectId: projectId })
|
||||
).unwrap();
|
||||
if (response.done && response.body) {
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
dispatch(fetchStatuses(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to create status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupBy === IGroupBy.PHASE && projectId) {
|
||||
const body = {
|
||||
name: sectionName,
|
||||
project_id: projectId,
|
||||
};
|
||||
|
||||
if (groupBy === IGroupBy.PHASE) {
|
||||
try {
|
||||
const response = await phasesApiService.addPhaseOption(projectId);
|
||||
if (response.done && response.body) {
|
||||
@@ -111,6 +147,19 @@ const EnhancedKanbanCreateSection: React.FC = () => {
|
||||
logger.error('Failed to create phase:', error);
|
||||
}
|
||||
}
|
||||
setIsAdding(false);
|
||||
setSectionName('');
|
||||
setSelectedCategoryId('');
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreateSection();
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsAdding(false);
|
||||
setSectionName('');
|
||||
setSelectedCategoryId('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -136,19 +185,105 @@ const EnhancedKanbanCreateSection: React.FC = () => {
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
borderRadius: 6,
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddSection}
|
||||
>
|
||||
{t('addSectionButton')}
|
||||
</Button>
|
||||
{isAdding ? (
|
||||
<div ref={dropdownRef} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{/* Header-like area */}
|
||||
<div
|
||||
className="enhanced-kanban-group-header flex items-center gap-2"
|
||||
style={{
|
||||
backgroundColor: headerBackgroundColor,
|
||||
borderRadius: 6,
|
||||
padding: '8px 12px',
|
||||
marginBottom: 8,
|
||||
minHeight: 36,
|
||||
}}
|
||||
>
|
||||
{/* Borderless input */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={sectionName}
|
||||
onChange={e => setSectionName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
className={`bg-transparent border-none outline-none text-sm font-semibold capitalize min-w-[120px] flex-1 ${themeMode === 'dark' ? 'text-gray-800 placeholder-gray-800' : 'text-gray-800 placeholder-gray-600'}`}
|
||||
placeholder={t('untitledSection')}
|
||||
style={{ marginBottom: 0 }}
|
||||
/>
|
||||
{/* Category selector dropdown */}
|
||||
{groupBy === IGroupBy.STATUS && statusCategories && statusCategories.length > 0 && (
|
||||
<div className="relative" ref={categoryDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
style={{ minWidth: 80 }}
|
||||
onClick={() => setShowCategoryDropdown(v => !v)}
|
||||
>
|
||||
<span className={themeMode === 'dark' ? 'text-gray-800' : 'text-gray-900'} style={{ fontSize: 13 }}>
|
||||
{selectedCategory?.name || t('changeCategory')}
|
||||
</span>
|
||||
<DownOutlined style={{ fontSize: 12, color: themeMode === 'dark' ? '#555' : '#555' }} />
|
||||
</button>
|
||||
{showCategoryDropdown && (
|
||||
<div
|
||||
className="absolute right-0 mt-1 w-30 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50"
|
||||
style={{ zIndex: 1000 }}
|
||||
>
|
||||
<div className="py-1">
|
||||
{statusCategories.filter(cat => typeof cat.id === 'string').map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
type="button"
|
||||
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
||||
onClick={() => {
|
||||
if (typeof cat.id === 'string') setSelectedCategoryId(cat.id);
|
||||
setShowCategoryDropdown(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: cat.color_code }}
|
||||
></div>
|
||||
<span className={selectedCategoryId === cat.id ? 'font-bold' : ''}>{cat.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={handleCreateSection}
|
||||
disabled={!sectionName.trim()}
|
||||
>
|
||||
{t('addSectionButton')}
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={() => { setIsAdding(false); setSectionName(''); setSelectedCategoryId(''); setShowCategoryDropdown(false); }}
|
||||
>
|
||||
{t('deleteConfirmationCancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
borderRadius: 6,
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddSection}
|
||||
>
|
||||
{t('addSectionButton')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user