feat(task-statuses): add category update functionality and enhance localization
- Implemented updateCategory method in TaskStatusesController to allow updating task status categories. - Added corresponding route for category updates in statusesApiRouter. - Enhanced task management localization by adding new translation keys for category-related actions in multiple languages. - Updated TaskGroupHeader component to support category changes with a modal for selecting categories.
This commit is contained in:
@@ -134,6 +134,25 @@ export default class TaskStatusesController extends WorklenzControllerBase {
|
|||||||
return res.status(200).send(new ServerResponse(true, data));
|
return res.status(200).send(new ServerResponse(true, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async updateCategory(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
|
const hasMoreCategories = await TaskStatusesController.hasMoreCategories(req.params.id, req.query.current_project_id as string);
|
||||||
|
|
||||||
|
if (!hasMoreCategories)
|
||||||
|
return res.status(200).send(new ServerResponse(false, null, existsErrorMessage).withTitle("Status category update failed!"));
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
UPDATE task_statuses
|
||||||
|
SET category_id = $2
|
||||||
|
WHERE id = $1
|
||||||
|
AND project_id = $3
|
||||||
|
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id), (SELECT color_code_dark FROM sys_task_status_categories WHERE id = task_statuses.category_id);
|
||||||
|
`;
|
||||||
|
const result = await db.query(q, [req.params.id, req.body.category_id, req.query.current_project_id]);
|
||||||
|
const [data] = result.rows;
|
||||||
|
return res.status(200).send(new ServerResponse(true, data));
|
||||||
|
}
|
||||||
|
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const q = `SELECT update_status_order($1);`;
|
const q = `SELECT update_status_order($1);`;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ statusesApiRouter.put("/order", statusOrderValidator, safeControllerFunction(Tas
|
|||||||
statusesApiRouter.get("/categories", safeControllerFunction(TaskStatusesController.getCategories));
|
statusesApiRouter.get("/categories", safeControllerFunction(TaskStatusesController.getCategories));
|
||||||
statusesApiRouter.get("/:id", idParamValidator, safeControllerFunction(TaskStatusesController.getById));
|
statusesApiRouter.get("/:id", idParamValidator, safeControllerFunction(TaskStatusesController.getById));
|
||||||
statusesApiRouter.put("/name/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.updateName));
|
statusesApiRouter.put("/name/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.updateName));
|
||||||
|
statusesApiRouter.put("/category/:id", projectManagerValidator, idParamValidator, safeControllerFunction(TaskStatusesController.updateCategory));
|
||||||
statusesApiRouter.put("/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.update));
|
statusesApiRouter.put("/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.update));
|
||||||
statusesApiRouter.delete("/:id", projectManagerValidator, idParamValidator, statusDeleteValidator, safeControllerFunction(TaskStatusesController.deleteById));
|
statusesApiRouter.delete("/:id", projectManagerValidator, idParamValidator, statusDeleteValidator, safeControllerFunction(TaskStatusesController.deleteById));
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,11 @@
|
|||||||
"attachments": "bashkëngjitje",
|
"attachments": "bashkëngjitje",
|
||||||
"enterSubtaskName": "Shkruani emrin e nën-detyrës...",
|
"enterSubtaskName": "Shkruani emrin e nën-detyrës...",
|
||||||
"add": "Shto",
|
"add": "Shto",
|
||||||
"cancel": "Anulo"
|
"cancel": "Anulo",
|
||||||
|
"renameGroup": "Riemërto Grupin",
|
||||||
|
"renameStatus": "Riemërto Statusin",
|
||||||
|
"renamePhase": "Riemërto Fazën",
|
||||||
|
"changeCategory": "Ndrysho Kategorinë",
|
||||||
|
"clickToEditGroupName": "Kliko për të ndryshuar emrin e grupit",
|
||||||
|
"enterGroupName": "Shkruani emrin e grupit"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,11 @@
|
|||||||
"attachments": "Anhänge",
|
"attachments": "Anhänge",
|
||||||
"enterSubtaskName": "Unteraufgabenname eingeben...",
|
"enterSubtaskName": "Unteraufgabenname eingeben...",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"cancel": "Abbrechen"
|
"cancel": "Abbrechen",
|
||||||
|
"renameGroup": "Gruppe umbenennen",
|
||||||
|
"renameStatus": "Status umbenennen",
|
||||||
|
"renamePhase": "Phase umbenennen",
|
||||||
|
"changeCategory": "Kategorie ändern",
|
||||||
|
"clickToEditGroupName": "Klicken Sie, um den Gruppennamen zu bearbeiten",
|
||||||
|
"enterGroupName": "Gruppennamen eingeben"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,11 @@
|
|||||||
"attachments": "attachments",
|
"attachments": "attachments",
|
||||||
"enterSubtaskName": "Enter subtask name...",
|
"enterSubtaskName": "Enter subtask name...",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel",
|
||||||
|
"renameGroup": "Rename Group",
|
||||||
|
"renameStatus": "Rename Status",
|
||||||
|
"renamePhase": "Rename Phase",
|
||||||
|
"changeCategory": "Change Category",
|
||||||
|
"clickToEditGroupName": "Click to edit group name",
|
||||||
|
"enterGroupName": "Enter group name"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,11 @@
|
|||||||
"attachments": "adjuntos",
|
"attachments": "adjuntos",
|
||||||
"enterSubtaskName": "Ingresa el nombre de la subtarea...",
|
"enterSubtaskName": "Ingresa el nombre de la subtarea...",
|
||||||
"add": "Añadir",
|
"add": "Añadir",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar",
|
||||||
|
"renameGroup": "Renombrar Grupo",
|
||||||
|
"renameStatus": "Renombrar Estado",
|
||||||
|
"renamePhase": "Renombrar Fase",
|
||||||
|
"changeCategory": "Cambiar Categoría",
|
||||||
|
"clickToEditGroupName": "Haz clic para editar el nombre del grupo",
|
||||||
|
"enterGroupName": "Ingresa el nombre del grupo"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,11 @@
|
|||||||
"attachments": "anexos",
|
"attachments": "anexos",
|
||||||
"enterSubtaskName": "Digite o nome da subtarefa...",
|
"enterSubtaskName": "Digite o nome da subtarefa...",
|
||||||
"add": "Adicionar",
|
"add": "Adicionar",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar",
|
||||||
|
"renameGroup": "Renomear Grupo",
|
||||||
|
"renameStatus": "Renomear Status",
|
||||||
|
"renamePhase": "Renomear Fase",
|
||||||
|
"changeCategory": "Alterar Categoria",
|
||||||
|
"clickToEditGroupName": "Clique para editar o nome do grupo",
|
||||||
|
"enterGroupName": "Digite o nome do grupo"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ export const statusApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateStatusCategory: async (
|
||||||
|
statusId: string,
|
||||||
|
categoryId: string,
|
||||||
|
currentProjectId: string
|
||||||
|
): Promise<IServerResponse<ITaskStatus>> => {
|
||||||
|
const q = toQueryString({ current_project_id: currentProjectId });
|
||||||
|
|
||||||
|
const response = await apiClient.put<IServerResponse<ITaskStatus>>(
|
||||||
|
`${rootUrl}/category/${statusId}${q}`,
|
||||||
|
{ category_id: categoryId }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
updateStatusOrder: async (
|
updateStatusOrder: async (
|
||||||
body: ITaskStatusCreateRequest,
|
body: ITaskStatusCreateRequest,
|
||||||
currentProjectId: string
|
currentProjectId: string
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
// @ts-ignore: Heroicons module types
|
// @ts-ignore: Heroicons module types
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { ChevronDownIcon, ChevronRightIcon, EllipsisHorizontalIcon, PencilIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||||
import { Checkbox } from 'antd';
|
import { Checkbox, Dropdown, Menu, Input, Modal, Badge, Flex } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getContrastColor } from '@/utils/colorUtils';
|
import { getContrastColor } from '@/utils/colorUtils';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { selectSelectedTaskIds, selectTask, deselectTask } from '@/features/task-management/selection.slice';
|
import { selectSelectedTaskIds, selectTask, deselectTask } from '@/features/task-management/selection.slice';
|
||||||
import { selectGroups } from '@/features/task-management/task-management.slice';
|
import { selectGroups, fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||||
|
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||||
|
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||||
|
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
|
||||||
|
import { fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
|
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types';
|
||||||
|
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
|
import { evt_project_board_column_setting_click } from '@/shared/worklenz-analytics-events';
|
||||||
|
|
||||||
interface TaskGroupHeaderProps {
|
interface TaskGroupHeaderProps {
|
||||||
group: {
|
group: {
|
||||||
@@ -18,12 +30,25 @@ interface TaskGroupHeaderProps {
|
|||||||
};
|
};
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, onToggle }) => {
|
const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, onToggle, projectId }) => {
|
||||||
|
const { t } = useTranslation('task-management');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedTaskIds = useAppSelector(selectSelectedTaskIds);
|
const selectedTaskIds = useAppSelector(selectSelectedTaskIds);
|
||||||
const groups = useAppSelector(selectGroups);
|
const groups = useAppSelector(selectGroups);
|
||||||
|
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
||||||
|
const { statusCategories } = useAppSelector(state => state.taskStatusReducer);
|
||||||
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
|
const { isOwnerOrAdmin } = useAuthService();
|
||||||
|
|
||||||
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
|
const [categoryModalVisible, setCategoryModalVisible] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [isChangingCategory, setIsChangingCategory] = useState(false);
|
||||||
|
const [isEditingName, setIsEditingName] = useState(false);
|
||||||
|
const [editingName, setEditingName] = useState(group.name);
|
||||||
|
|
||||||
const headerBackgroundColor = group.color || '#F0F0F0'; // Default light gray if no color
|
const headerBackgroundColor = group.color || '#F0F0F0'; // Default light gray if no color
|
||||||
const headerTextColor = getContrastColor(headerBackgroundColor);
|
const headerTextColor = getContrastColor(headerBackgroundColor);
|
||||||
@@ -67,6 +92,139 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
}
|
}
|
||||||
}, [dispatch, isAllSelected, tasksInGroup]);
|
}, [dispatch, isAllSelected, tasksInGroup]);
|
||||||
|
|
||||||
|
// Handle inline name editing
|
||||||
|
const handleNameSave = useCallback(async () => {
|
||||||
|
if (!editingName.trim() || editingName.trim() === group.name || isRenaming) return;
|
||||||
|
|
||||||
|
setIsRenaming(true);
|
||||||
|
try {
|
||||||
|
if (currentGrouping === 'status') {
|
||||||
|
// Extract status ID from group ID (format: "status-{statusId}")
|
||||||
|
const statusId = group.id.replace('status-', '');
|
||||||
|
const body: ITaskStatusUpdateModel = {
|
||||||
|
name: editingName.trim(),
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await statusApiService.updateNameOfStatus(statusId, body, projectId);
|
||||||
|
trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Status' });
|
||||||
|
dispatch(fetchStatuses(projectId));
|
||||||
|
|
||||||
|
} else if (currentGrouping === 'phase') {
|
||||||
|
// Extract phase ID from group ID (format: "phase-{phaseId}")
|
||||||
|
const phaseId = group.id.replace('phase-', '');
|
||||||
|
const body = { id: phaseId, name: editingName.trim() };
|
||||||
|
|
||||||
|
await phasesApiService.updateNameOfPhase(phaseId, body as ITaskPhase, projectId);
|
||||||
|
trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' });
|
||||||
|
dispatch(fetchPhasesByProjectId(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh task list to get updated group names
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
setIsEditingName(false);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error renaming group:', error);
|
||||||
|
setEditingName(group.name);
|
||||||
|
} finally {
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
}, [editingName, group.name, group.id, currentGrouping, projectId, dispatch, trackMixpanelEvent, isRenaming]);
|
||||||
|
|
||||||
|
const handleNameClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!isOwnerOrAdmin) return;
|
||||||
|
setIsEditingName(true);
|
||||||
|
setEditingName(group.name);
|
||||||
|
}, [group.name, isOwnerOrAdmin]);
|
||||||
|
|
||||||
|
const handleNameKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleNameSave();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsEditingName(false);
|
||||||
|
setEditingName(group.name);
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
}, [group.name, handleNameSave]);
|
||||||
|
|
||||||
|
const handleNameBlur = useCallback(() => {
|
||||||
|
setIsEditingName(false);
|
||||||
|
setEditingName(group.name);
|
||||||
|
}, [group.name]);
|
||||||
|
|
||||||
|
// Handle dropdown menu actions
|
||||||
|
const handleRenameGroup = useCallback(() => {
|
||||||
|
setDropdownVisible(false);
|
||||||
|
setIsEditingName(true);
|
||||||
|
setEditingName(group.name);
|
||||||
|
}, [group.name]);
|
||||||
|
|
||||||
|
const handleChangeCategory = useCallback(() => {
|
||||||
|
setDropdownVisible(false);
|
||||||
|
setCategoryModalVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Handle category change
|
||||||
|
const handleCategoryChange = useCallback(async (categoryId: string, e?: React.MouseEvent) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
if (isChangingCategory) return;
|
||||||
|
|
||||||
|
setIsChangingCategory(true);
|
||||||
|
try {
|
||||||
|
// Extract status ID from group ID (format: "status-{statusId}")
|
||||||
|
const statusId = group.id.replace('status-', '');
|
||||||
|
|
||||||
|
await statusApiService.updateStatusCategory(statusId, categoryId, projectId);
|
||||||
|
trackMixpanelEvent(evt_project_board_column_setting_click, { 'Change category': 'Status' });
|
||||||
|
|
||||||
|
// Refresh status list and tasks
|
||||||
|
dispatch(fetchStatuses(projectId));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
setCategoryModalVisible(false);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error changing category:', error);
|
||||||
|
} finally {
|
||||||
|
setIsChangingCategory(false);
|
||||||
|
}
|
||||||
|
}, [group.id, projectId, dispatch, trackMixpanelEvent, isChangingCategory]);
|
||||||
|
|
||||||
|
// Create dropdown menu items
|
||||||
|
const menuItems = useMemo(() => {
|
||||||
|
if (!isOwnerOrAdmin) return [];
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'rename',
|
||||||
|
icon: <PencilIcon className="h-4 w-4" />,
|
||||||
|
label: currentGrouping === 'status' ? t('renameStatus') : currentGrouping === 'phase' ? t('renamePhase') : t('renameGroup'),
|
||||||
|
onClick: (e: any) => {
|
||||||
|
e?.domEvent?.stopPropagation();
|
||||||
|
handleRenameGroup();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only show "Change Category" when grouped by status
|
||||||
|
if (currentGrouping === 'status') {
|
||||||
|
items.push({
|
||||||
|
key: 'changeCategory',
|
||||||
|
icon: <ArrowPathIcon className="h-4 w-4" />,
|
||||||
|
label: t('changeCategory'),
|
||||||
|
onClick: (e: any) => {
|
||||||
|
e?.domEvent?.stopPropagation();
|
||||||
|
handleChangeCategory();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [currentGrouping, handleRenameGroup, handleChangeCategory, isOwnerOrAdmin]);
|
||||||
|
|
||||||
// Make the group header droppable
|
// Make the group header droppable
|
||||||
const { isOver, setNodeRef } = useDroppable({
|
const { isOver, setNodeRef } = useDroppable({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
@@ -133,12 +291,108 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
{/* Group indicator and name - no gap at all */}
|
{/* Group indicator and name - no gap at all */}
|
||||||
<div className="flex items-center flex-1 ml-1">
|
<div className="flex items-center flex-1 ml-1">
|
||||||
{/* Group name and count */}
|
{/* Group name and count */}
|
||||||
<div className="flex items-center flex-1">
|
<div className="flex items-center">
|
||||||
<span className="text-sm font-semibold">
|
{isEditingName && isOwnerOrAdmin ? (
|
||||||
{group.name} ({group.count})
|
<Input
|
||||||
|
value={editingName}
|
||||||
|
onChange={(e) => setEditingName(e.target.value)}
|
||||||
|
onKeyDown={handleNameKeyDown}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
className="text-sm font-semibold px-2 py-1 rounded-md transition-all duration-200 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
||||||
|
style={{
|
||||||
|
color: headerTextColor,
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 600,
|
||||||
|
width: `${Math.max(editingName.length * 8 + 16, 80)}px`,
|
||||||
|
minWidth: '80px',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: `1px solid ${headerTextColor}40`,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
color: headerTextColor,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
padding: '0'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
disabled={isRenaming}
|
||||||
|
placeholder={t('enterGroupName')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`text-sm font-semibold ${isOwnerOrAdmin ? 'cursor-pointer hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10 rounded px-2 py-1 transition-all duration-200 hover:shadow-sm' : ''}`}
|
||||||
|
onClick={handleNameClick}
|
||||||
|
style={{ color: headerTextColor }}
|
||||||
|
title={isOwnerOrAdmin ? t('clickToEditGroupName') : ''}
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-semibold ml-1" style={{ color: headerTextColor }}>
|
||||||
|
({group.count})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Three dots menu */}
|
||||||
|
<div className="flex items-center justify-center ml-2">
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: menuItems }}
|
||||||
|
trigger={['click']}
|
||||||
|
open={dropdownVisible}
|
||||||
|
onOpenChange={setDropdownVisible}
|
||||||
|
placement="bottomLeft"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="p-1 rounded-sm hover:bg-black hover:bg-opacity-10 transition-all duration-200 ease-out"
|
||||||
|
style={{ color: headerTextColor }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDropdownVisible(!dropdownVisible);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Change Category Modal */}
|
||||||
|
<Modal
|
||||||
|
title="Change Category"
|
||||||
|
open={categoryModalVisible}
|
||||||
|
onCancel={() => setCategoryModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{statusCategories?.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.id}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
onClick={(e) => category.id && handleCategoryChange(category.id, e)}
|
||||||
|
>
|
||||||
|
<Flex align="center" gap={12}>
|
||||||
|
<Badge color={category.color_code} />
|
||||||
|
<span className="font-medium">{category.name}</span>
|
||||||
|
</Flex>
|
||||||
|
{isChangingCategory && (
|
||||||
|
<div className="text-blue-500">
|
||||||
|
<ArrowPathIcon className="h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ const TaskListV2: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
isCollapsed={isGroupCollapsed}
|
isCollapsed={isGroupCollapsed}
|
||||||
onToggle={() => handleGroupCollapse(group.id)}
|
onToggle={() => handleGroupCollapse(group.id)}
|
||||||
|
projectId={urlProjectId || ''}
|
||||||
/>
|
/>
|
||||||
{isGroupEmpty && !isGroupCollapsed && (
|
{isGroupEmpty && !isGroupCollapsed && (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
|
|||||||
Reference in New Issue
Block a user