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:
@@ -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);
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
setSelectedCategoryId(statusCategories[0].id);
|
||||||
|
} else {
|
||||||
|
setSelectedCategoryId('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (groupBy === IGroupBy.STATUS && projectId) {
|
const handleCreateSection = async () => {
|
||||||
// Find the "To do" category
|
if (!sectionName.trim() || !projectId) return;
|
||||||
const todoCategory = statusCategories.find(
|
const name = getUniqueSectionName(sectionName.trim());
|
||||||
category =>
|
if (groupBy === IGroupBy.STATUS && selectedCategoryId) {
|
||||||
category.name?.toLowerCase() === 'to do' || category.name?.toLowerCase() === 'todo'
|
const body = {
|
||||||
);
|
name,
|
||||||
|
project_id: projectId,
|
||||||
if (todoCategory && todoCategory.id) {
|
category_id: selectedCategoryId,
|
||||||
// Create a new status
|
};
|
||||||
const body = {
|
try {
|
||||||
name: sectionName,
|
const response = await dispatch(
|
||||||
project_id: projectId,
|
createStatus({ body, currentProjectId: projectId })
|
||||||
category_id: todoCategory.id,
|
).unwrap();
|
||||||
};
|
if (response.done && response.body) {
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
try {
|
dispatch(fetchStatuses(projectId));
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
} catch (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,19 +185,105 @@ const EnhancedKanbanCreateSection: React.FC = () => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
{isAdding ? (
|
||||||
type="text"
|
<div ref={dropdownRef} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
style={{
|
{/* Header-like area */}
|
||||||
height: '38px',
|
<div
|
||||||
width: '100%',
|
className="enhanced-kanban-group-header flex items-center gap-2"
|
||||||
borderRadius: 6,
|
style={{
|
||||||
boxShadow: 'none',
|
backgroundColor: headerBackgroundColor,
|
||||||
}}
|
borderRadius: 6,
|
||||||
icon={<PlusOutlined />}
|
padding: '8px 12px',
|
||||||
onClick={handleAddSection}
|
marginBottom: 8,
|
||||||
>
|
minHeight: 36,
|
||||||
{t('addSectionButton')}
|
}}
|
||||||
</Button>
|
>
|
||||||
|
{/* 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>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user