Merge pull request #256 from shancds/test/row-kanban-board-v1.1.8

feat(enhanced-kanban): enhance section creation with category selecti…
This commit is contained in:
Chamika J
2025-07-11 14:07:09 +05:30
committed by GitHub
2 changed files with 191 additions and 56 deletions

View File

@@ -108,9 +108,9 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
const updateStatus = async (category = group.category_id ?? null) => { const updateStatus = async (category = group.category_id ?? null) => {
if (!category || !projectId || !group.id) return; if (!category || !projectId || !group.id) return;
const sectionName = getUniqueSectionName(name); // const sectionName = getUniqueSectionName(name);
const body: ITaskStatusUpdateModel = { const body: ITaskStatusUpdateModel = {
name: sectionName, name: name.trim(),
project_id: projectId, project_id: projectId,
category_id: category, category_id: category,
}; };
@@ -118,7 +118,7 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
if (res.done) { if (res.done) {
dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchEnhancedKanbanGroups(projectId));
dispatch(fetchStatuses(projectId)); dispatch(fetchStatuses(projectId));
setName(sectionName); setName(name.trim());
} else { } else {
setName(editName); setName(editName);
logger.error('Error updating status', res.message); logger.error('Error updating status', res.message);

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Button, Flex } from 'antd'; import { Button, Flex } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { nanoid } from '@reduxjs/toolkit'; import { nanoid } from '@reduxjs/toolkit';
import { DownOutlined } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { themeWiseColor } from '@/utils/themeWiseColor'; import { themeWiseColor } from '@/utils/themeWiseColor';
@@ -33,6 +34,52 @@ const EnhancedKanbanCreateSection: React.FC = () => {
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin(); const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
const isProjectManager = useIsProjectManager(); 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 // Don't show for priority grouping or if user doesn't have permissions
if (groupBy === IGroupBy.PRIORITY || (!isOwnerorAdmin && !isProjectManager)) { if (groupBy === IGroupBy.PRIORITY || (!isOwnerorAdmin && !isProjectManager)) {
return null; return null;
@@ -59,49 +106,38 @@ const EnhancedKanbanCreateSection: React.FC = () => {
}; };
const handleAddSection = async () => { const handleAddSection = async () => {
const sectionId = nanoid(); setIsAdding(true);
const baseNameSection = 'Untitled section'; setSectionName('');
const sectionName = getUniqueSectionName(baseNameSection); // Default to first category if available
if (statusCategories && statusCategories.length > 0 && typeof statusCategories[0].id === 'string') {
if (groupBy === IGroupBy.STATUS && projectId) { setSelectedCategoryId(statusCategories[0].id);
// Find the "To do" category } else {
const todoCategory = statusCategories.find( setSelectedCategoryId('');
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,
}; };
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 { try {
// Create the status
const response = await dispatch( const response = await dispatch(
createStatus({ body, currentProjectId: projectId }) createStatus({ body, currentProjectId: projectId })
).unwrap(); ).unwrap();
if (response.done && response.body) { if (response.done && response.body) {
// Refresh the board to show the new section
dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchEnhancedKanbanGroups(projectId));
// Refresh statuses
dispatch(fetchStatuses(projectId)); dispatch(fetchStatuses(projectId));
} }
} catch (error) { } catch (error) {
logger.error('Failed to create status:', error); logger.error('Failed to create status:', error);
} }
} }
} if (groupBy === IGroupBy.PHASE) {
if (groupBy === IGroupBy.PHASE && projectId) {
const body = {
name: sectionName,
project_id: projectId,
};
try { try {
const response = await phasesApiService.addPhaseOption(projectId); const response = await phasesApiService.addPhaseOption(projectId);
if (response.done && response.body) { if (response.done && response.body) {
@@ -111,6 +147,19 @@ const EnhancedKanbanCreateSection: React.FC = () => {
logger.error('Failed to create phase:', error); 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 ( return (
@@ -136,6 +185,91 @@ const EnhancedKanbanCreateSection: React.FC = () => {
), ),
}} }}
> >
{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 <Button
type="text" type="text"
style={{ style={{
@@ -149,6 +283,7 @@ const EnhancedKanbanCreateSection: React.FC = () => {
> >
{t('addSectionButton')} {t('addSectionButton')}
</Button> </Button>
)}
</div> </div>
</Flex> </Flex>
); );