diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx index ce934d34..399e9f77 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx @@ -7,7 +7,30 @@ import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard'; import VirtualizedTaskList from './VirtualizedTaskList'; import { useAppSelector } from '@/hooks/useAppSelector'; import './EnhancedKanbanGroup.css'; - +import { Badge, Flex, InputRef, MenuProps, Popconfirm } from 'antd'; +import { themeWiseColor } from '@/utils/themeWiseColor'; +import useIsProjectManager from '@/hooks/useIsProjectManager'; +import { useAuthService } from '@/hooks/useAuth'; +import { DeleteOutlined, ExclamationCircleFilled, EditOutlined, LoadingOutlined, RetweetOutlined, MoreOutlined } from '@ant-design/icons/lib/icons'; +import { colors } from '@/styles/colors'; +import { Input } from 'antd'; +import { Tooltip } from 'antd'; +import { Typography } from 'antd'; +import { Dropdown } from 'antd'; +import { Button } from 'antd'; +import { PlusOutlined } from '@ant-design/icons/lib/icons'; +import { deleteSection, IGroupBy, setBoardGroupName } from '@/features/board/board-slice'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { useTranslation } from 'react-i18next'; +import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types'; +import { statusApiService } from '@/api/taskAttributes/status/status.api.service'; +import { fetchStatuses } from '@/features/taskAttributes/taskStatusSlice'; +import logger from '@/utils/errorLogger'; +import { evt_project_board_column_setting_click } from '@/shared/worklenz-analytics-events'; +import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service'; +import { ITaskPhase } from '@/types/tasks/taskPhase.types'; +import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; +import { deleteStatusToggleDrawer, seletedStatusCategory } from '@/features/projects/status/DeleteStatusSlice'; interface EnhancedKanbanGroupProps { group: ITaskListGroup; activeTaskId?: string | null; @@ -22,7 +45,23 @@ const EnhancedKanbanGroup: React.FC = React.memo(({ activeTaskId, overId }) => { + const [isHover, setIsHover] = useState(false); + const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin(); + const [isEditable, setIsEditable] = useState(false); + const isProjectManager = useIsProjectManager(); + const [isLoading, setIsLoading] = useState(false); + const [name, setName] = useState(group.name); + const inputRef = useRef(null); + const [editName, setEdit] = useState(group.name); + const [isEllipsisActive, setIsEllipsisActive] = useState(false); const themeMode = useAppSelector(state => state.themeReducer.mode); + const dispatch = useAppDispatch(); + const { projectId } = useAppSelector(state => state.projectReducer); + const { groupBy } = useAppSelector(state => state.boardReducer); + const { statusCategories, status } = useAppSelector(state => state.taskStatusReducer); + const { trackMixpanelEvent } = useMixpanelTracking(); + const [showNewCard, setShowNewCard] = useState(false); + const { t } = useTranslation('kanban-board'); const { setNodeRef: setDroppableRef, isOver } = useDroppable({ id: group.id, @@ -99,6 +138,51 @@ const EnhancedKanbanGroup: React.FC = React.memo(({ transition, opacity: isGroupDragging ? 0.5 : 1, }; + const getUniqueSectionName = (baseName: string): string => { + // Check if the base name already exists + const existingNames = status.map(status => status.name?.toLowerCase()); + + if (!existingNames.includes(baseName.toLowerCase())) { + return baseName; + } + + // If the base name exists, add a number suffix + let counter = 1; + let newName = `${baseName.trim()} (${counter})`; + + while (existingNames.includes(newName.toLowerCase())) { + counter++; + newName = `${baseName.trim()} (${counter})`; + } + + return newName; + }; + const updateStatus = async (category = group.category_id ?? null) => { + if (!category || !projectId || !group.id) return; + const sectionName = getUniqueSectionName(name); + const body: ITaskStatusUpdateModel = { + name: sectionName, + project_id: projectId, + category_id: category, + }; + const res = await statusApiService.updateStatus(group.id, body, projectId); + if (res.done) { + dispatch( + setBoardGroupName({ + groupId: group.id, + name: sectionName ?? '', + colorCode: res.body.color_code ?? '', + colorCodeDark: res.body.color_code_dark ?? '', + categoryId: category, + }) + ); + dispatch(fetchStatuses(projectId)); + setName(sectionName); + } else { + setName(editName); + logger.error('Error updating status', res.message); + } + }; // Get the appropriate background color based on theme const headerBackgroundColor = useMemo(() => { @@ -108,12 +192,128 @@ const EnhancedKanbanGroup: React.FC = React.memo(({ return group.color_code || '#f5f5f5'; }, [themeMode, group.color_code, group.color_code_dark]); + const handleChange = async (e: React.ChangeEvent) => { + const taskName = e.target.value; + setName(taskName); + }; + + const handleBlur = async () => { + if (group.name === 'Untitled section') { + dispatch(deleteSection({ sectionId: group.id })); + } + setIsEditable(false); + + if (!projectId || !group.id) return; + + if (groupBy === IGroupBy.STATUS) { + await updateStatus(); + } + + if (groupBy === IGroupBy.PHASE) { + const body = { + id: group.id, + name: name, + }; + + const res = await phasesApiService.updateNameOfPhase(group.id, body as ITaskPhase, projectId); + if (res.done) { + trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' }); + // dispatch(fetchPhasesByProjectId(projectId)); + } + } + }; + + const handlePressEnter = () => { + setShowNewCard(true); + setIsEditable(false); + handleBlur(); + }; + const handleDeleteSection = async () => { + if (!projectId || !group.id) return; + + try { + if (groupBy === IGroupBy.STATUS) { + const replacingStatusId = ''; + const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId); + if (res.message === 'At least one status should exists under each category.') return + if (res.done) { + dispatch(deleteSection({ sectionId: group.id })); + } else { + dispatch(seletedStatusCategory({ id: group.id, name: name, category_id: group.category_id ?? '', message: res.message ?? '' })); + dispatch(deleteStatusToggleDrawer()); + } + } else if (groupBy === IGroupBy.PHASE) { + const res = await phasesApiService.deletePhaseOption(group.id, projectId); + if (res.done) { + dispatch(deleteSection({ sectionId: group.id })); + } + } + } catch (error) { + logger.error('Error deleting section', error); + } + }; + const items: MenuProps['items'] = [ + { + key: '1', + label: ( +
setIsEditable(true)} + > + {t('rename')} +
+ ), + }, + groupBy === IGroupBy.STATUS && { + key: '2', + icon: , + label: 'Change category', + children: statusCategories?.map(status => ({ + key: status.id, + label: ( + status.id && updateStatus(status.id)} + style={group.category_id === status.id ? { fontWeight: 700 } : {}} + > + + {status.name} + + ), + })), + }, + groupBy !== IGroupBy.PRIORITY && { + key: '3', + label: ( + } + okText={t('deleteConfirmationOk')} + cancelText={t('deleteConfirmationCancel')} + onConfirm={handleDeleteSection} + > + + + {t('delete')} + + + ), + }, + ].filter(Boolean) as MenuProps['items']; + + return (
+ {/* section header */}
= React.memo(({ {...attributes} {...listeners} > -

{group.name}

- ({group.tasks.length}) - {shouldVirtualize && ( + {/* ({group.tasks.length}) */} + setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + > + { + if ((isProjectManager || isOwnerOrAdmin) && group.name !== 'Unmapped') setIsEditable(true); + }} + > + + {group.tasks.length} + + + {isLoading && } + {isEditable ? ( + + ) : ( + + setIsEllipsisActive(ellipsed), + }} + style={{ + minWidth: 185, + textTransform: 'capitalize', + color: themeMode === 'dark' ? '#383838' : '', + display: 'inline-block', + overflow: 'hidden', + }} + > + {name} + + + )} + + +
+ + + {(isOwnerOrAdmin || isProjectManager) && name !== 'Unmapped' && ( + + + + )} +
+
+ {/*

{group.name}

*/} + + {/* {shouldVirtualize && ( - )} + )} */}