feat(task-management): enhance bulk action bar and localization updates
- Added new features to the OptimizedBulkActionBar, including dropdowns for labels and assignees, improving task management capabilities. - Integrated task template creation functionality for owners/admins, allowing users to create templates from selected tasks. - Updated localization files for multiple languages, adding new strings for label searching and template name requirements to enhance user experience. - Refactored LabelsDropdown component to support label filtering and improved UI feedback for label creation.
This commit is contained in:
@@ -17,7 +17,9 @@
|
||||
"createTaskTemplate": "Krijo Shabllon Detyre",
|
||||
"apply": "Apliko",
|
||||
"createLabel": "+ Krijo Etiketë",
|
||||
"searchOrCreateLabel": "Kërko ose krijo etiketë...",
|
||||
"hitEnterToCreate": "Shtyp Enter për të krijuar",
|
||||
"labelExists": "Etiketa ekziston tashmë",
|
||||
"pendingInvitation": "Ftesë në Pritje",
|
||||
"noMatchingLabels": "Asnjë etiketë që përputhet",
|
||||
"noLabels": "Asnjë etiketë"
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"createTaskTemplate": "Aufgabenvorlage erstellen",
|
||||
"apply": "Anwenden",
|
||||
"createLabel": "+ Label erstellen",
|
||||
"searchOrCreateLabel": "Label suchen oder erstellen...",
|
||||
"hitEnterToCreate": "Enter drücken zum Erstellen",
|
||||
"labelExists": "Label existiert bereits",
|
||||
"pendingInvitation": "Einladung ausstehend",
|
||||
"noMatchingLabels": "Keine passenden Labels",
|
||||
"noLabels": "Keine Labels",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"cancelText": "Cancel",
|
||||
"saveText": "Save",
|
||||
"templateNameText": "Template Name",
|
||||
"templateNameRequired": "Template name is required",
|
||||
"selectedTasks": "Selected Tasks",
|
||||
"removeTask": "Remove",
|
||||
"cancelButton": "Cancel",
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"createTaskTemplate": "Create Task Template",
|
||||
"apply": "Apply",
|
||||
"createLabel": "+ Create Label",
|
||||
"searchOrCreateLabel": "Search or create label...",
|
||||
"hitEnterToCreate": "Press Enter to create",
|
||||
"labelExists": "Label already exists",
|
||||
"pendingInvitation": "Pending Invitation",
|
||||
"noMatchingLabels": "No matching labels",
|
||||
"noLabels": "No labels",
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"createTaskTemplate": "Crear plantilla de tarea",
|
||||
"apply": "Aplicar",
|
||||
"createLabel": "+ Crear etiqueta",
|
||||
"searchOrCreateLabel": "Buscar o crear etiqueta...",
|
||||
"hitEnterToCreate": "Presione Enter para crear",
|
||||
"labelExists": "La etiqueta ya existe",
|
||||
"pendingInvitation": "Invitación Pendiente",
|
||||
"noMatchingLabels": "No hay etiquetas coincidentes",
|
||||
"noLabels": "Sin etiquetas",
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"createTaskTemplate": "Criar Modelo de Tarefa",
|
||||
"apply": "Aplicar",
|
||||
"createLabel": "+ Criar etiqueta",
|
||||
"searchOrCreateLabel": "Pesquisar ou criar etiqueta...",
|
||||
"hitEnterToCreate": "Pressione Enter para criar",
|
||||
"labelExists": "A etiqueta já existe",
|
||||
"pendingInvitation": "Convite Pendente",
|
||||
"noMatchingLabels": "Nenhuma etiqueta correspondente",
|
||||
"noLabels": "Sem etiquetas",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
Button,
|
||||
@@ -19,12 +19,23 @@ import {
|
||||
TagsOutlined,
|
||||
UsergroupAddOutlined,
|
||||
FlagOutlined,
|
||||
BulbOutlined
|
||||
BulbOutlined,
|
||||
MoreOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { RootState } from '@/app/store';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { selectTasks } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { useBulkActionTranslations } from '@/hooks/useTranslationPreloader';
|
||||
import LabelsDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/LabelsDropdown';
|
||||
import AssigneesDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/AssigneesDropdown';
|
||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
|
||||
import { InputRef } from 'antd/es/input';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import TaskTemplateDrawer from '@/components/task-templates/task-template-drawer';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -139,12 +150,16 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
onBulkSetDueDate,
|
||||
}) => {
|
||||
const { t, ready, isLoading } = useBulkActionTranslations();
|
||||
const dispatch = useDispatch();
|
||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||
|
||||
// Get data from Redux store
|
||||
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
||||
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
|
||||
const phaseList = useAppSelector(state => state.phaseReducer.phaseList);
|
||||
const labelsList = useAppSelector(state => state.taskLabelsReducer.labels);
|
||||
const members = useAppSelector(state => state.teamMembersReducer.teamMembers);
|
||||
const tasks = useAppSelector(state => state.taskManagement.entities);
|
||||
|
||||
// Performance state management
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
@@ -162,6 +177,20 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
dueDate: false,
|
||||
});
|
||||
|
||||
// Labels dropdown state
|
||||
const [selectedLabels, setSelectedLabels] = useState<ITaskLabel[]>([]);
|
||||
const [createLabelText, setCreateLabelText] = useState<string>('');
|
||||
const labelsInputRef = useRef<InputRef>(null);
|
||||
|
||||
// Assignees dropdown state
|
||||
const [assigneeDropdownOpen, setAssigneeDropdownOpen] = useState(false);
|
||||
|
||||
// Task template state
|
||||
const [showDrawer, setShowDrawer] = useState(false);
|
||||
|
||||
// Auth service for permissions
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
|
||||
// Smooth entrance animation
|
||||
useEffect(() => {
|
||||
if (totalSelected > 0) {
|
||||
@@ -200,6 +229,8 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
})), [phaseList]
|
||||
);
|
||||
|
||||
|
||||
|
||||
// Menu click handlers
|
||||
const handleStatusMenuClick = useCallback((e: any) => {
|
||||
onBulkStatusChange?.(e.key);
|
||||
@@ -213,6 +244,126 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
onBulkPhaseChange?.(e.key);
|
||||
}, [onBulkPhaseChange]);
|
||||
|
||||
const handleLabelsMenuClick = useCallback((e: any) => {
|
||||
onBulkAddLabels?.([e.key]);
|
||||
}, [onBulkAddLabels]);
|
||||
|
||||
// Labels dropdown handlers
|
||||
const handleLabelChange = useCallback((e: CheckboxChangeEvent, label: ITaskLabel) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedLabels(prev => [...prev, label]);
|
||||
} else {
|
||||
setSelectedLabels(prev => prev.filter(l => l.id !== label.id));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleApplyLabels = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
updateLoadingState('labels', true);
|
||||
const body = {
|
||||
tasks: selectedTaskIds,
|
||||
labels: selectedLabels,
|
||||
text: selectedLabels.length > 0 ? null : createLabelText.trim() !== '' ? createLabelText.trim() : null,
|
||||
};
|
||||
await onBulkAddLabels?.(selectedLabels.map(l => l.id).filter((id): id is string => id !== undefined));
|
||||
setCreateLabelText('');
|
||||
setSelectedLabels([]);
|
||||
} catch (error) {
|
||||
// Error handling is done in the parent component
|
||||
} finally {
|
||||
updateLoadingState('labels', false);
|
||||
}
|
||||
}, [selectedLabels, createLabelText, selectedTaskIds, projectId, onBulkAddLabels, updateLoadingState]);
|
||||
|
||||
// Assignees dropdown handlers
|
||||
const handleChangeAssignees = useCallback(async (selectedAssignees: ITeamMemberViewModel[]) => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
updateLoadingState('assignMembers', true);
|
||||
await onBulkAssignMembers?.(selectedAssignees.map(m => m.id).filter((id): id is string => id !== undefined));
|
||||
} catch (error) {
|
||||
// Error handling is done in the parent component
|
||||
} finally {
|
||||
updateLoadingState('assignMembers', false);
|
||||
}
|
||||
}, [projectId, onBulkAssignMembers, updateLoadingState]);
|
||||
|
||||
const onAssigneeDropdownOpenChange = useCallback((open: boolean) => {
|
||||
setAssigneeDropdownOpen(open);
|
||||
}, []);
|
||||
|
||||
// Get selected task objects for template creation
|
||||
const selectedTaskObjects = useMemo(() => {
|
||||
return Object.values(tasks).filter((task: any) => selectedTaskIds.includes(task.id));
|
||||
}, [tasks, selectedTaskIds]);
|
||||
|
||||
// Update Redux state when opening template drawer
|
||||
const handleOpenTemplateDrawer = useCallback(() => {
|
||||
// Convert Task objects to IProjectTask format for template creation
|
||||
const projectTasks: IProjectTask[] = selectedTaskObjects.map((task: any) => ({
|
||||
id: task.id,
|
||||
name: task.title, // Always use title as the name
|
||||
task_key: task.task_key,
|
||||
status: task.status,
|
||||
status_id: task.status,
|
||||
priority: task.priority,
|
||||
phase_id: task.phase,
|
||||
phase_name: task.phase,
|
||||
description: task.description,
|
||||
start_date: task.startDate,
|
||||
end_date: task.dueDate,
|
||||
total_hours: task.timeTracking?.estimated || 0,
|
||||
total_minutes: task.timeTracking?.logged || 0,
|
||||
progress: task.progress,
|
||||
sub_tasks_count: task.sub_tasks_count || 0,
|
||||
assignees: task.assignees?.map((assigneeId: string) => ({
|
||||
id: assigneeId,
|
||||
name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
team_member_id: assigneeId,
|
||||
project_member_id: assigneeId,
|
||||
})) || [],
|
||||
labels: task.labels || [],
|
||||
manual_progress: false,
|
||||
created_at: task.createdAt,
|
||||
updated_at: task.updatedAt,
|
||||
sort_order: task.order,
|
||||
}));
|
||||
|
||||
// Update the bulkActionReducer with selected tasks
|
||||
dispatch(selectTasks(projectTasks));
|
||||
setShowDrawer(true);
|
||||
}, [selectedTaskObjects, dispatch]);
|
||||
|
||||
// Labels dropdown content
|
||||
const labelsDropdownContent = useMemo(() => (
|
||||
<LabelsDropdown
|
||||
labelsList={labelsList || []}
|
||||
themeMode={isDarkMode ? 'dark' : 'light'}
|
||||
createLabelText={createLabelText}
|
||||
selectedLabels={selectedLabels}
|
||||
labelsInputRef={labelsInputRef as React.RefObject<InputRef>}
|
||||
onLabelChange={handleLabelChange}
|
||||
onCreateLabelTextChange={setCreateLabelText}
|
||||
onApply={handleApplyLabels}
|
||||
t={t}
|
||||
loading={loadingStates.labels}
|
||||
/>
|
||||
), [labelsList, isDarkMode, createLabelText, selectedLabels, handleLabelChange, handleApplyLabels, t, loadingStates.labels]);
|
||||
|
||||
// Assignees dropdown content
|
||||
const assigneesDropdownContent = useMemo(() => (
|
||||
<AssigneesDropdown
|
||||
members={members?.data || []}
|
||||
themeMode={isDarkMode ? 'dark' : 'light'}
|
||||
onApply={handleChangeAssignees}
|
||||
onClose={() => setAssigneeDropdownOpen(false)}
|
||||
t={t}
|
||||
/>
|
||||
), [members?.data, isDarkMode, handleChangeAssignees, t]);
|
||||
|
||||
// Memoized handlers with loading states
|
||||
const handleStatusChange = useCallback(async () => {
|
||||
updateLoadingState('status', true);
|
||||
@@ -466,13 +617,41 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
</Tooltip>
|
||||
|
||||
{/* Change Labels */}
|
||||
<ActionButton
|
||||
icon={<TagsOutlined />}
|
||||
tooltip={t('ADD_LABELS')}
|
||||
onClick={() => onBulkAddLabels?.([])}
|
||||
loading={loadingStates.labels}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<Tooltip title={t('ADD_LABELS')} placement="top">
|
||||
<Dropdown
|
||||
dropdownRender={() => labelsDropdownContent}
|
||||
trigger={['click']}
|
||||
placement="top"
|
||||
arrow
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setSelectedLabels([]);
|
||||
setCreateLabelText('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<TagsOutlined />}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
color: isDarkMode ? '#e5e7eb' : '#374151',
|
||||
border: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '6px',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
size="small"
|
||||
type="text"
|
||||
loading={loadingStates.labels}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
|
||||
{/* Assign to Me */}
|
||||
<ActionButton
|
||||
@@ -484,13 +663,37 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
/>
|
||||
|
||||
{/* Change Assignees */}
|
||||
<ActionButton
|
||||
icon={<UsergroupAddOutlined />}
|
||||
tooltip={t('ASSIGN_MEMBERS')}
|
||||
onClick={() => onBulkAssignMembers?.([])}
|
||||
loading={loadingStates.assignMembers}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<Tooltip title={t('ASSIGN_MEMBERS')} placement="top">
|
||||
<Dropdown
|
||||
dropdownRender={() => assigneesDropdownContent}
|
||||
open={assigneeDropdownOpen}
|
||||
onOpenChange={onAssigneeDropdownOpenChange}
|
||||
trigger={['click']}
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<Button
|
||||
icon={<UsergroupAddOutlined />}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
color: isDarkMode ? '#e5e7eb' : '#374151',
|
||||
border: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '6px',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
size="small"
|
||||
type="text"
|
||||
loading={loadingStates.assignMembers}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
|
||||
{/* Archive */}
|
||||
<ActionButton
|
||||
@@ -520,6 +723,46 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
/>
|
||||
</Popconfirm>
|
||||
|
||||
{/* More Options (Create Task Template) - Only for owners/admins */}
|
||||
{isOwnerOrAdmin && (
|
||||
<Tooltip title={t('moreOptions')} placement="top">
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: '1',
|
||||
label: t('createTaskTemplate'),
|
||||
onClick: handleOpenTemplateDrawer,
|
||||
},
|
||||
],
|
||||
}}
|
||||
placement="top"
|
||||
arrow
|
||||
>
|
||||
<Button
|
||||
icon={<MoreOutlined />}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
color: isDarkMode ? '#e5e7eb' : '#374151',
|
||||
border: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '6px',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Divider
|
||||
type="vertical"
|
||||
style={{
|
||||
@@ -537,6 +780,20 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{/* Task Template Drawer */}
|
||||
{createPortal(
|
||||
<TaskTemplateDrawer
|
||||
showDrawer={showDrawer}
|
||||
selectedTemplateId={null}
|
||||
onClose={() => {
|
||||
setShowDrawer(false);
|
||||
onClearSelection?.();
|
||||
}}
|
||||
/>,
|
||||
document.body,
|
||||
'create-task-template'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,6 +38,11 @@ import {
|
||||
toggleTaskSelection,
|
||||
clearSelection,
|
||||
} from '@/features/task-management/selection.slice';
|
||||
import {
|
||||
selectTaskIds,
|
||||
selectTasks,
|
||||
deselectAll as deselectAllBulk,
|
||||
} from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
@@ -49,7 +54,6 @@ import OptimizedBulkActionBar from './optimized-bulk-action-bar';
|
||||
import VirtualizedTaskList from './virtualized-task-list';
|
||||
import { AppDispatch } from '@/app/store';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service';
|
||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
import {
|
||||
@@ -73,6 +77,7 @@ import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||
import alertService from '@/services/alerts/alertService';
|
||||
import logger from '@/utils/errorLogger';
|
||||
@@ -153,7 +158,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const tasks = useSelector(taskManagementSelectors.selectAll);
|
||||
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
|
||||
const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual);
|
||||
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
||||
// Use bulk action slice for selected tasks instead of selection slice
|
||||
const selectedTaskIds = useSelector((state: RootState) => state.bulkActionReducer.selectedTaskIdsList);
|
||||
const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks);
|
||||
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
||||
const error = useSelector((state: RootState) => state.taskManagement.error);
|
||||
|
||||
@@ -403,9 +410,53 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
|
||||
const handleSelectTask = useCallback(
|
||||
(taskId: string, selected: boolean) => {
|
||||
dispatch(toggleTaskSelection(taskId));
|
||||
if (selected) {
|
||||
// Add task to bulk selection
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
// Convert Task to IProjectTask format for bulk actions
|
||||
const projectTask: IProjectTask = {
|
||||
id: task.id,
|
||||
name: task.title, // Always use title as the name
|
||||
task_key: task.task_key,
|
||||
status: task.status,
|
||||
status_id: task.status,
|
||||
priority: task.priority,
|
||||
phase_id: task.phase,
|
||||
phase_name: task.phase,
|
||||
description: task.description,
|
||||
start_date: task.startDate,
|
||||
end_date: task.dueDate,
|
||||
total_hours: task.timeTracking.estimated || 0,
|
||||
total_minutes: task.timeTracking.logged || 0,
|
||||
progress: task.progress,
|
||||
sub_tasks_count: task.sub_tasks_count || 0,
|
||||
assignees: task.assignees.map(assigneeId => ({
|
||||
id: assigneeId,
|
||||
name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
team_member_id: assigneeId,
|
||||
project_member_id: assigneeId,
|
||||
})),
|
||||
labels: task.labels,
|
||||
manual_progress: false, // Default value for Task type
|
||||
created_at: task.createdAt,
|
||||
updated_at: task.updatedAt,
|
||||
sort_order: task.order,
|
||||
};
|
||||
dispatch(selectTasks([...selectedTasks, projectTask]));
|
||||
dispatch(selectTaskIds([...selectedTaskIds, taskId]));
|
||||
}
|
||||
} else {
|
||||
// Remove task from bulk selection
|
||||
const updatedTasks = selectedTasks.filter(t => t.id !== taskId);
|
||||
const updatedTaskIds = selectedTaskIds.filter(id => id !== taskId);
|
||||
dispatch(selectTasks(updatedTasks));
|
||||
dispatch(selectTaskIds(updatedTaskIds));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, selectedTasks, selectedTaskIds, tasks]
|
||||
);
|
||||
|
||||
const handleToggleSubtasks = useCallback((taskId: string) => {
|
||||
@@ -430,7 +481,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
|
||||
// Bulk action handlers - implementing real functionality from task-list-bulk-actions-bar
|
||||
const handleClearSelection = useCallback(() => {
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -468,7 +519,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.changeStatus(body, projectId);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
@@ -490,7 +541,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.changePriority(body, projectId);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
@@ -512,7 +563,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.changePhase(body, projectId);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
@@ -531,7 +582,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.assignToMe(body);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_assign_me);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
@@ -563,7 +614,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.assignTasks(body);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
@@ -588,7 +639,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.assignLabels(body, projectId);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
dispatch(fetchLabels());
|
||||
@@ -608,7 +659,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.archiveTasks(body, archived);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_archive);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
@@ -627,7 +678,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const res = await taskListBulkActionsApiService.deleteTasks(body, projectId);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_delete);
|
||||
dispatch(deselectAll());
|
||||
dispatch(deselectAllBulk());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ const TaskTemplateDrawer = ({
|
||||
fetchTemplateData();
|
||||
return;
|
||||
}
|
||||
// Tasks should already have the name property set correctly
|
||||
setTemplateData({ tasks: selectedTasks });
|
||||
};
|
||||
|
||||
@@ -126,7 +127,7 @@ const TaskTemplateDrawer = ({
|
||||
open={showDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
afterOpenChange={afterOpenChange}
|
||||
destroyOnClose={true}
|
||||
destroyOnHidden={true}
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'right' }}>
|
||||
<Button onClick={onCloseDrawer}>{t('cancelButton')}</Button>
|
||||
|
||||
@@ -35,6 +35,23 @@ const LabelsDropdown = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Filter labels based on search input
|
||||
const filteredLabels = useMemo(() => {
|
||||
if (!createLabelText.trim()) {
|
||||
return labelsList;
|
||||
}
|
||||
return labelsList.filter(label =>
|
||||
label.name?.toLowerCase().includes(createLabelText.toLowerCase())
|
||||
);
|
||||
}, [labelsList, createLabelText]);
|
||||
|
||||
// Check if the search text matches any existing label exactly
|
||||
const exactMatch = useMemo(() => {
|
||||
return labelsList.some(label =>
|
||||
label.name?.toLowerCase() === createLabelText.toLowerCase()
|
||||
);
|
||||
}, [labelsList, createLabelText]);
|
||||
|
||||
const isOnApply = () => {
|
||||
if (!createLabelText.trim() && selectedLabels.length === 0) return;
|
||||
onApply();
|
||||
@@ -42,18 +59,17 @@ const LabelsDropdown = ({
|
||||
return (
|
||||
<Card className="custom-card" styles={{ body: { padding: 8 } }}>
|
||||
<Flex vertical>
|
||||
{/* Always show the list, filtered by input */}
|
||||
{!createLabelText && (
|
||||
<List
|
||||
style={{
|
||||
padding: 0,
|
||||
overflow: 'auto',
|
||||
maxHeight: labelsList.length > 10 ? '200px' : 'auto', // Set max height if more than 10 labels
|
||||
maxWidth: 250,
|
||||
{/* Show filtered labels list */}
|
||||
<List
|
||||
style={{
|
||||
padding: 0,
|
||||
overflow: 'auto',
|
||||
maxHeight: filteredLabels.length > 10 ? '200px' : 'auto',
|
||||
maxWidth: 250,
|
||||
}}
|
||||
>
|
||||
{labelsList.length > 0 && (
|
||||
labelsList.map(label => (
|
||||
{filteredLabels.length > 0 ? (
|
||||
filteredLabels.map(label => (
|
||||
<List.Item
|
||||
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
|
||||
key={label.id}
|
||||
@@ -75,30 +91,68 @@ const LabelsDropdown = ({
|
||||
</Checkbox>
|
||||
</List.Item>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
) : createLabelText.trim() ? (
|
||||
<List.Item
|
||||
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
justifyContent: 'flex-start',
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('noMatchingLabels')}
|
||||
</Typography.Text>
|
||||
</List.Item>
|
||||
) : (
|
||||
<List.Item
|
||||
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
justifyContent: 'flex-start',
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('noLabels')}
|
||||
</Typography.Text>
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<Flex style={{ paddingTop: 8 }} vertical justify="space-between" gap={8}>
|
||||
<Input
|
||||
ref={labelsInputRef}
|
||||
value={createLabelText}
|
||||
onChange={e => onCreateLabelTextChange(e.currentTarget.value)}
|
||||
placeholder={t('createLabel')}
|
||||
placeholder={t('searchOrCreateLabel')}
|
||||
onPressEnter={() => {
|
||||
isOnApply();
|
||||
}}
|
||||
/>
|
||||
{createLabelText && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('hitEnterToCreate')}
|
||||
{exactMatch
|
||||
? t('labelExists')
|
||||
: t('hitEnterToCreate')
|
||||
}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{!createLabelText && (
|
||||
<Button type="primary" size="small" onClick={isOnApply} style={{ width: '100%' }}>
|
||||
{t('apply')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={isOnApply}
|
||||
style={{ width: '100%' }}
|
||||
disabled={!createLabelText.trim() && selectedLabels.length === 0}
|
||||
>
|
||||
{t('apply')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user