feat(localization): update and enhance localization files for multiple languages
- Updated localization files for various languages, including English, German, Spanish, Portuguese, and Chinese, to ensure consistency and accuracy across the application. - Added new keys and updated existing ones to support recent UI changes and features, particularly in project views, task lists, and admin center settings. - Enhanced the structure of localization files to improve maintainability and facilitate future updates. - Implemented performance optimizations in the frontend components to better handle localization data.
This commit is contained in:
@@ -106,7 +106,9 @@
|
||||
|
||||
/* Sortable item styling */
|
||||
.sortable-status-item {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sortable-status-item.is-dragging {
|
||||
@@ -144,11 +146,11 @@
|
||||
width: 95% !important;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
.dark-modal .ant-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.status-item {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -173,7 +175,9 @@
|
||||
.status-item-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
.status-item-exit {
|
||||
@@ -184,5 +188,7 @@
|
||||
.status-item-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Button, Tabs, Space, Divider, Typography, Flex, DatePicker, Select } from '@/shared/antd-imports';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Tabs,
|
||||
Space,
|
||||
Divider,
|
||||
Typography,
|
||||
Flex,
|
||||
DatePicker,
|
||||
Select,
|
||||
} from '@/shared/antd-imports';
|
||||
import { PlusOutlined, DragOutlined } from '@/shared/antd-imports';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
@@ -11,7 +23,11 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import TaskDetailsForm from '@/components/task-drawer/shared/info-tab/task-details-form';
|
||||
import AssigneeSelector from '@/components/AssigneeSelector';
|
||||
import LabelsSelector from '@/components/LabelsSelector';
|
||||
import { createStatus, fetchStatuses, fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||
import {
|
||||
createStatus,
|
||||
fetchStatuses,
|
||||
fetchStatusesCategories,
|
||||
} from '@/features/taskAttributes/taskStatusSlice';
|
||||
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||
import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types';
|
||||
import { Modal as AntModal } from '@/shared/antd-imports';
|
||||
@@ -49,14 +65,9 @@ const SortableStatusItem: React.FC<StatusItemProps & { id: string }> = ({
|
||||
const [editName, setEditName] = useState(status.name || '');
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -76,13 +87,16 @@ const SortableStatusItem: React.FC<StatusItemProps & { id: string }> = ({
|
||||
setIsEditing(false);
|
||||
}, [status.name]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
}, [handleSave, handleCancel]);
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
},
|
||||
[handleSave, handleCancel]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
@@ -127,7 +141,7 @@ const SortableStatusItem: React.FC<StatusItemProps & { id: string }> = ({
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={handleKeyDown}
|
||||
size="small"
|
||||
@@ -151,7 +165,9 @@ const SortableStatusItem: React.FC<StatusItemProps & { id: string }> = ({
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className={isDarkMode ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-600'}
|
||||
className={
|
||||
isDarkMode ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-600'
|
||||
}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
@@ -176,7 +192,7 @@ const StatusManagement: React.FC<{
|
||||
}> = ({ projectId, isDarkMode }) => {
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
const { status: statuses } = useAppSelector(state => state.taskStatusReducer);
|
||||
const [localStatuses, setLocalStatuses] = useState(statuses);
|
||||
const [newStatusName, setNewStatusName] = useState('');
|
||||
@@ -201,9 +217,9 @@ const StatusManagement: React.FC<{
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalStatuses((items) => {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
setLocalStatuses(items => {
|
||||
const oldIndex = items.findIndex(item => item.id === active.id);
|
||||
const newIndex = items.findIndex(item => item.id === over.id);
|
||||
|
||||
if (oldIndex === -1 || newIndex === -1) return items;
|
||||
|
||||
@@ -228,7 +244,7 @@ const StatusManagement: React.FC<{
|
||||
try {
|
||||
const statusCategories = await dispatch(fetchStatusesCategories()).unwrap();
|
||||
const defaultCategory = statusCategories[0]?.id;
|
||||
|
||||
|
||||
if (!defaultCategory) {
|
||||
console.error('No status categories found');
|
||||
return;
|
||||
@@ -250,35 +266,41 @@ const StatusManagement: React.FC<{
|
||||
}
|
||||
}, [newStatusName, projectId, dispatch]);
|
||||
|
||||
const handleRenameStatus = useCallback(async (id: string, name: string) => {
|
||||
try {
|
||||
const body: ITaskStatusUpdateModel = {
|
||||
name: name.trim(),
|
||||
project_id: projectId,
|
||||
};
|
||||
|
||||
await statusApiService.updateNameOfStatus(id, body, projectId);
|
||||
dispatch(fetchStatuses(projectId));
|
||||
} catch (error) {
|
||||
console.error('Error renaming status:', error);
|
||||
}
|
||||
}, [projectId, dispatch]);
|
||||
const handleRenameStatus = useCallback(
|
||||
async (id: string, name: string) => {
|
||||
try {
|
||||
const body: ITaskStatusUpdateModel = {
|
||||
name: name.trim(),
|
||||
project_id: projectId,
|
||||
};
|
||||
|
||||
const handleDeleteStatus = useCallback(async (id: string) => {
|
||||
AntModal.confirm({
|
||||
title: 'Delete Status',
|
||||
content: 'Are you sure you want to delete this status? This action cannot be undone.',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const replacingStatusId = localStatuses.find(s => s.id !== id)?.id || '';
|
||||
await statusApiService.deleteStatus(id, projectId, replacingStatusId);
|
||||
dispatch(fetchStatuses(projectId));
|
||||
} catch (error) {
|
||||
console.error('Error deleting status:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [localStatuses, projectId, dispatch]);
|
||||
await statusApiService.updateNameOfStatus(id, body, projectId);
|
||||
dispatch(fetchStatuses(projectId));
|
||||
} catch (error) {
|
||||
console.error('Error renaming status:', error);
|
||||
}
|
||||
},
|
||||
[projectId, dispatch]
|
||||
);
|
||||
|
||||
const handleDeleteStatus = useCallback(
|
||||
async (id: string) => {
|
||||
AntModal.confirm({
|
||||
title: 'Delete Status',
|
||||
content: 'Are you sure you want to delete this status? This action cannot be undone.',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const replacingStatusId = localStatuses.find(s => s.id !== id)?.id || '';
|
||||
await statusApiService.deleteStatus(id, projectId, replacingStatusId);
|
||||
dispatch(fetchStatuses(projectId));
|
||||
} catch (error) {
|
||||
console.error('Error deleting status:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[localStatuses, projectId, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -296,7 +318,7 @@ const StatusManagement: React.FC<{
|
||||
<Input
|
||||
placeholder="Enter status name"
|
||||
value={newStatusName}
|
||||
onChange={(e) => setNewStatusName(e.target.value)}
|
||||
onChange={e => setNewStatusName(e.target.value)}
|
||||
onPressEnter={handleCreateStatus}
|
||||
className="flex-1"
|
||||
/>
|
||||
@@ -319,16 +341,18 @@ const StatusManagement: React.FC<{
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{localStatuses.filter(status => status.id).map((status) => (
|
||||
<SortableStatusItem
|
||||
key={status.id}
|
||||
id={status.id!}
|
||||
status={status}
|
||||
onRename={handleRenameStatus}
|
||||
onDelete={handleDeleteStatus}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
))}
|
||||
{localStatuses
|
||||
.filter(status => status.id)
|
||||
.map(status => (
|
||||
<SortableStatusItem
|
||||
key={status.id}
|
||||
id={status.id!}
|
||||
status={status}
|
||||
onRename={handleRenameStatus}
|
||||
onDelete={handleDeleteStatus}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
@@ -342,29 +366,25 @@ const StatusManagement: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
projectId,
|
||||
}) => {
|
||||
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({ open, onClose, projectId }) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const [form] = Form.useForm();
|
||||
const [activeTab, setActiveTab] = useState('task-info');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
// Redux state
|
||||
const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark');
|
||||
const currentProjectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
const user = useAppSelector(state => state.auth?.user);
|
||||
|
||||
|
||||
const finalProjectId = projectId || currentProjectId;
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
|
||||
const { socket } = useSocket();
|
||||
|
||||
|
||||
if (!socket || !user || !finalProjectId) {
|
||||
console.error('Missing socket, user, or project ID');
|
||||
return;
|
||||
@@ -383,15 +403,14 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
|
||||
|
||||
// Create task via socket
|
||||
socket.emit(SocketEvents.QUICK_TASK.toString(), taskData);
|
||||
|
||||
|
||||
// Refresh task list
|
||||
dispatch(fetchTasksV3(finalProjectId));
|
||||
|
||||
|
||||
// Reset form and close modal
|
||||
form.resetFields();
|
||||
setActiveTab('task-info');
|
||||
onClose();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
@@ -423,9 +442,7 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
|
||||
<Flex justify="space-between" align="center">
|
||||
<div></div>
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>{t('cancel')}</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
{t('createTask')}
|
||||
</Button>
|
||||
@@ -465,65 +482,49 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('description')}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('descriptionPlaceholder')}
|
||||
rows={4}
|
||||
maxLength={1000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label={t('description')}>
|
||||
<Input.TextArea
|
||||
placeholder={t('descriptionPlaceholder')}
|
||||
rows={4}
|
||||
maxLength={1000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Status Selection */}
|
||||
<Form.Item
|
||||
name="status"
|
||||
label={t('status')}
|
||||
rules={[{ required: true, message: 'Please select a status' }]}
|
||||
>
|
||||
<Select placeholder="Select status">
|
||||
{/* TODO: Populate with actual statuses */}
|
||||
<Select.Option value="todo">To Do</Select.Option>
|
||||
<Select.Option value="inprogress">In Progress</Select.Option>
|
||||
<Select.Option value="done">Done</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{/* Status Selection */}
|
||||
<Form.Item
|
||||
name="status"
|
||||
label={t('status')}
|
||||
rules={[{ required: true, message: 'Please select a status' }]}
|
||||
>
|
||||
<Select placeholder="Select status">
|
||||
{/* TODO: Populate with actual statuses */}
|
||||
<Select.Option value="todo">To Do</Select.Option>
|
||||
<Select.Option value="inprogress">In Progress</Select.Option>
|
||||
<Select.Option value="done">Done</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* Priority Selection */}
|
||||
<Form.Item
|
||||
name="priority"
|
||||
label={t('priority')}
|
||||
>
|
||||
<Select placeholder="Select priority">
|
||||
<Select.Option value="low">Low</Select.Option>
|
||||
<Select.Option value="medium">Medium</Select.Option>
|
||||
<Select.Option value="high">High</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{/* Priority Selection */}
|
||||
<Form.Item name="priority" label={t('priority')}>
|
||||
<Select placeholder="Select priority">
|
||||
<Select.Option value="low">Low</Select.Option>
|
||||
<Select.Option value="medium">Medium</Select.Option>
|
||||
<Select.Option value="high">High</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* Assignees */}
|
||||
<Form.Item
|
||||
name="assignees"
|
||||
label={t('assignees')}
|
||||
>
|
||||
<Select mode="multiple" placeholder="Select assignees">
|
||||
{/* TODO: Populate with team members */}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{/* Assignees */}
|
||||
<Form.Item name="assignees" label={t('assignees')}>
|
||||
<Select mode="multiple" placeholder="Select assignees">
|
||||
{/* TODO: Populate with team members */}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* Due Date */}
|
||||
<Form.Item
|
||||
name="dueDate"
|
||||
label={t('dueDate')}
|
||||
>
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
placeholder="Select due date"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Due Date */}
|
||||
<Form.Item name="dueDate" label={t('dueDate')}>
|
||||
<DatePicker className="w-full" placeholder="Select due date" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
),
|
||||
@@ -533,10 +534,7 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
|
||||
label: t('manageStatuses'),
|
||||
children: finalProjectId ? (
|
||||
<div className="py-4">
|
||||
<StatusManagement
|
||||
projectId={finalProjectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<StatusManagement projectId={finalProjectId} isDarkMode={isDarkMode} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`text-center py-8 ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
@@ -550,4 +548,4 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTaskModal;
|
||||
export default CreateTaskModal;
|
||||
|
||||
@@ -276,7 +276,9 @@
|
||||
|
||||
/* Sortable item styling */
|
||||
.sortable-phase-item {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sortable-phase-item.is-dragging {
|
||||
@@ -311,15 +313,15 @@
|
||||
width: 95% !important;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
.dark-modal .ant-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.phase-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.phase-color-picker {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@@ -354,7 +356,9 @@
|
||||
.phase-item-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
.phase-item-exit {
|
||||
@@ -365,7 +369,9 @@
|
||||
.phase-item-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Loading state styling */
|
||||
@@ -380,4 +386,4 @@
|
||||
/* Divider styling for dark mode */
|
||||
.dark-modal .ant-divider-horizontal {
|
||||
border-color: #303030;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Button, Space, Divider, Typography, Flex, ColorPicker, Tooltip } from '@/shared/antd-imports';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
Divider,
|
||||
Typography,
|
||||
Flex,
|
||||
ColorPicker,
|
||||
Tooltip,
|
||||
} from '@/shared/antd-imports';
|
||||
import { PlusOutlined, HolderOutlined, EditOutlined, DeleteOutlined } from '@/shared/antd-imports';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
@@ -58,14 +69,9 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -85,13 +91,16 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
setIsEditing(false);
|
||||
}, [phase.name]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
}, [handleSave, handleCancel]);
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
},
|
||||
[handleSave, handleCancel]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setIsEditing(true);
|
||||
@@ -132,8 +141,8 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`flex-shrink-0 cursor-grab active:cursor-grabbing p-1 rounded transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-600'
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-600'
|
||||
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
@@ -144,12 +153,12 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
<div className="flex-shrink-0 flex items-center gap-1">
|
||||
<ColorPicker
|
||||
value={color}
|
||||
onChange={(value) => setColor(value.toHexString())}
|
||||
onChange={value => setColor(value.toHexString())}
|
||||
onChangeComplete={handleColorChangeComplete}
|
||||
size="small"
|
||||
className="phase-color-picker"
|
||||
/>
|
||||
<div
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded border shadow-sm"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
@@ -164,12 +173,12 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`font-medium text-xs border-0 px-1 py-1 shadow-none ${
|
||||
isDarkMode
|
||||
? 'bg-transparent text-gray-200 placeholder-gray-400'
|
||||
isDarkMode
|
||||
? 'bg-transparent text-gray-200 placeholder-gray-400'
|
||||
: 'bg-transparent text-gray-900 placeholder-gray-500'
|
||||
}`}
|
||||
placeholder={t('enterPhaseName')}
|
||||
@@ -177,7 +186,9 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
) : (
|
||||
<Text
|
||||
className={`text-xs font-medium cursor-pointer transition-colors select-none ${
|
||||
isDarkMode ? 'text-gray-200 hover:text-gray-100' : 'text-gray-800 hover:text-gray-900'
|
||||
isDarkMode
|
||||
? 'text-gray-200 hover:text-gray-100'
|
||||
: 'text-gray-800 hover:text-gray-900'
|
||||
}`}
|
||||
onClick={handleClick}
|
||||
title={t('rename')}
|
||||
@@ -188,9 +199,11 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
</div>
|
||||
|
||||
{/* Hover Actions */}
|
||||
<div className={`flex items-center gap-1 transition-all duration-200 ${
|
||||
isHovered || isEditing ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
<div
|
||||
className={`flex items-center gap-1 transition-all duration-200 ${
|
||||
isHovered || isEditing ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
>
|
||||
<Tooltip title={t('rename')}>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -198,8 +211,8 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setIsEditing(true)}
|
||||
className={`h-6 w-6 flex items-center justify-center transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-600'
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-600'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
/>
|
||||
@@ -211,8 +224,8 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => onDelete(id)}
|
||||
className={`h-6 w-6 flex items-center justify-center transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'text-red-400 hover:text-red-300 hover:bg-red-800'
|
||||
isDarkMode
|
||||
? 'text-red-400 hover:text-red-300 hover:bg-red-800'
|
||||
: 'text-red-500 hover:text-red-600 hover:bg-red-50'
|
||||
}`}
|
||||
/>
|
||||
@@ -223,20 +236,16 @@ const SortablePhaseItem: React.FC<PhaseItemProps & { id: string }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
projectId,
|
||||
}) => {
|
||||
const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({ open, onClose, projectId }) => {
|
||||
const { t } = useTranslation('phases-drawer');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
// Redux state
|
||||
const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark');
|
||||
const currentProjectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
const { phaseList, loadingPhases } = useAppSelector(state => state.phaseReducer);
|
||||
|
||||
|
||||
const [phaseName, setPhaseName] = useState<string>(project?.phase_label || '');
|
||||
const [initialPhaseName, setInitialPhaseName] = useState<string>(project?.phase_label || '');
|
||||
const [sorting, setSorting] = useState(false);
|
||||
@@ -270,39 +279,42 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
}
|
||||
}, [finalProjectId, dispatch]);
|
||||
|
||||
const handleDragEnd = useCallback(async (event: DragEndEvent) => {
|
||||
if (!finalProjectId) return;
|
||||
const { active, over } = event;
|
||||
const handleDragEnd = useCallback(
|
||||
async (event: DragEndEvent) => {
|
||||
if (!finalProjectId) return;
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = phaseList.findIndex(item => item.id === active.id);
|
||||
const newIndex = phaseList.findIndex(item => item.id === over.id);
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = phaseList.findIndex(item => item.id === active.id);
|
||||
const newIndex = phaseList.findIndex(item => item.id === over.id);
|
||||
|
||||
const newPhaseList = [...phaseList];
|
||||
const [movedItem] = newPhaseList.splice(oldIndex, 1);
|
||||
newPhaseList.splice(newIndex, 0, movedItem);
|
||||
const newPhaseList = [...phaseList];
|
||||
const [movedItem] = newPhaseList.splice(oldIndex, 1);
|
||||
newPhaseList.splice(newIndex, 0, movedItem);
|
||||
|
||||
try {
|
||||
setSorting(true);
|
||||
dispatch(updatePhaseListOrder(newPhaseList));
|
||||
try {
|
||||
setSorting(true);
|
||||
dispatch(updatePhaseListOrder(newPhaseList));
|
||||
|
||||
const body = {
|
||||
from_index: oldIndex,
|
||||
to_index: newIndex,
|
||||
phases: newPhaseList,
|
||||
project_id: finalProjectId,
|
||||
};
|
||||
const body = {
|
||||
from_index: oldIndex,
|
||||
to_index: newIndex,
|
||||
phases: newPhaseList,
|
||||
project_id: finalProjectId,
|
||||
};
|
||||
|
||||
await dispatch(updatePhaseOrder({ projectId: finalProjectId, body })).unwrap();
|
||||
await refreshTasks();
|
||||
} catch (error) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
console.error('Error updating phase order', error);
|
||||
} finally {
|
||||
setSorting(false);
|
||||
await dispatch(updatePhaseOrder({ projectId: finalProjectId, body })).unwrap();
|
||||
await refreshTasks();
|
||||
} catch (error) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
console.error('Error updating phase order', error);
|
||||
} finally {
|
||||
setSorting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [finalProjectId, phaseList, dispatch, refreshTasks]);
|
||||
},
|
||||
[finalProjectId, phaseList, dispatch, refreshTasks]
|
||||
);
|
||||
|
||||
const handleCreatePhase = useCallback(async () => {
|
||||
if (!newPhaseName.trim() || !finalProjectId) return;
|
||||
@@ -318,96 +330,108 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
}
|
||||
}, [finalProjectId, dispatch, refreshTasks, newPhaseName]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreatePhase();
|
||||
} else if (e.key === 'Escape') {
|
||||
setNewPhaseName('');
|
||||
setShowAddForm(false);
|
||||
}
|
||||
}, [handleCreatePhase]);
|
||||
|
||||
const handleRenamePhase = useCallback(async (id: string, name: string) => {
|
||||
if (!finalProjectId) return;
|
||||
|
||||
try {
|
||||
const phase = phaseList.find(p => p.id === id);
|
||||
if (!phase) return;
|
||||
|
||||
const updatedPhase = { ...phase, name: name.trim() };
|
||||
const response = await dispatch(
|
||||
updatePhaseName({
|
||||
phaseId: id,
|
||||
phase: updatedPhase,
|
||||
projectId: finalProjectId,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
if (response.done) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreatePhase();
|
||||
} else if (e.key === 'Escape') {
|
||||
setNewPhaseName('');
|
||||
setShowAddForm(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renaming phase:', error);
|
||||
}
|
||||
}, [finalProjectId, phaseList, dispatch, refreshTasks]);
|
||||
},
|
||||
[handleCreatePhase]
|
||||
);
|
||||
|
||||
const handleDeletePhase = useCallback(async (id: string) => {
|
||||
if (!finalProjectId) return;
|
||||
|
||||
AntModal.confirm({
|
||||
title: t('deletePhase'),
|
||||
content: t('deletePhaseConfirm'),
|
||||
okText: t('delete'),
|
||||
cancelText: t('cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await dispatch(
|
||||
deletePhaseOption({ phaseOptionId: id, projectId: finalProjectId })
|
||||
).unwrap();
|
||||
const handleRenamePhase = useCallback(
|
||||
async (id: string, name: string) => {
|
||||
if (!finalProjectId) return;
|
||||
|
||||
if (response.done) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting phase:', error);
|
||||
try {
|
||||
const phase = phaseList.find(p => p.id === id);
|
||||
if (!phase) return;
|
||||
|
||||
const updatedPhase = { ...phase, name: name.trim() };
|
||||
const response = await dispatch(
|
||||
updatePhaseName({
|
||||
phaseId: id,
|
||||
phase: updatedPhase,
|
||||
projectId: finalProjectId,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
if (response.done) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [finalProjectId, dispatch, refreshTasks, t]);
|
||||
|
||||
const handleColorChange = useCallback(async (id: string, color: string) => {
|
||||
if (!finalProjectId) return;
|
||||
|
||||
try {
|
||||
const phase = phaseList.find(p => p.id === id);
|
||||
if (!phase) return;
|
||||
|
||||
const updatedPhase = { ...phase, color_code: color };
|
||||
const response = await dispatch(
|
||||
updatePhaseColor({ projectId: finalProjectId, body: updatedPhase })
|
||||
).unwrap();
|
||||
|
||||
if (response.done) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
} catch (error) {
|
||||
console.error('Error renaming phase:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error changing phase color:', error);
|
||||
}
|
||||
}, [finalProjectId, phaseList, dispatch, refreshTasks]);
|
||||
},
|
||||
[finalProjectId, phaseList, dispatch, refreshTasks]
|
||||
);
|
||||
|
||||
const handleDeletePhase = useCallback(
|
||||
async (id: string) => {
|
||||
if (!finalProjectId) return;
|
||||
|
||||
AntModal.confirm({
|
||||
title: t('deletePhase'),
|
||||
content: t('deletePhaseConfirm'),
|
||||
okText: t('delete'),
|
||||
cancelText: t('cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await dispatch(
|
||||
deletePhaseOption({ phaseOptionId: id, projectId: finalProjectId })
|
||||
).unwrap();
|
||||
|
||||
if (response.done) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting phase:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[finalProjectId, dispatch, refreshTasks, t]
|
||||
);
|
||||
|
||||
const handleColorChange = useCallback(
|
||||
async (id: string, color: string) => {
|
||||
if (!finalProjectId) return;
|
||||
|
||||
try {
|
||||
const phase = phaseList.find(p => p.id === id);
|
||||
if (!phase) return;
|
||||
|
||||
const updatedPhase = { ...phase, color_code: color };
|
||||
const response = await dispatch(
|
||||
updatePhaseColor({ projectId: finalProjectId, body: updatedPhase })
|
||||
).unwrap();
|
||||
|
||||
if (response.done) {
|
||||
dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error changing phase color:', error);
|
||||
}
|
||||
},
|
||||
[finalProjectId, phaseList, dispatch, refreshTasks]
|
||||
);
|
||||
|
||||
const handlePhaseNameBlur = useCallback(async () => {
|
||||
if (!finalProjectId || phaseName === initialPhaseName) return;
|
||||
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const res = await dispatch(
|
||||
updateProjectPhaseLabel({ projectId: finalProjectId, phaseLabel: phaseName })
|
||||
).unwrap();
|
||||
|
||||
|
||||
if (res.done) {
|
||||
dispatch(updatePhaseLabel(phaseName));
|
||||
setInitialPhaseName(phaseName);
|
||||
@@ -427,9 +451,10 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Title level={4} className={`m-0 font-semibold ${
|
||||
isDarkMode ? 'text-gray-100' : 'text-gray-800'
|
||||
}`}>
|
||||
<Title
|
||||
level={4}
|
||||
className={`m-0 font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{t('configure')} {phaseName || project?.phase_label || t('phasesText')}
|
||||
</Title>
|
||||
}
|
||||
@@ -445,14 +470,14 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
},
|
||||
}}
|
||||
footer={
|
||||
<div className={`flex justify-end pt-3 ${
|
||||
isDarkMode ? 'border-gray-700' : 'border-gray-200'
|
||||
}`}>
|
||||
<Button
|
||||
<div
|
||||
className={`flex justify-end pt-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
className={`font-medium ${
|
||||
isDarkMode
|
||||
? 'text-gray-300 hover:text-gray-200 border-gray-600'
|
||||
isDarkMode
|
||||
? 'text-gray-300 hover:text-gray-200 border-gray-600'
|
||||
: 'text-gray-600 hover:text-gray-800 border-gray-300'
|
||||
}`}
|
||||
>
|
||||
@@ -465,15 +490,17 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Phase Label Configuration */}
|
||||
<div className={`p-3 rounded border transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-800 border-gray-700 text-gray-300'
|
||||
: 'bg-blue-50 border-blue-200 text-blue-700'
|
||||
}`}>
|
||||
<div
|
||||
className={`p-3 rounded border transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-800 border-gray-700 text-gray-300'
|
||||
: 'bg-blue-50 border-blue-200 text-blue-700'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Text className={`text-xs font-medium ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-blue-700'
|
||||
}`}>
|
||||
<Text
|
||||
className={`text-xs font-medium ${isDarkMode ? 'text-gray-300' : 'text-blue-700'}`}
|
||||
>
|
||||
{t('phaseLabel')}
|
||||
</Text>
|
||||
<Input
|
||||
@@ -489,34 +516,40 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<div className={`p-3 rounded border transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-800 border-gray-700 text-gray-300'
|
||||
: 'bg-blue-50 border-blue-200 text-blue-700'
|
||||
}`}>
|
||||
<Text className={`text-xs font-medium ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-blue-700'
|
||||
}`}>
|
||||
🎨 Drag {(phaseName || project?.phase_label || t('phasesText')).toLowerCase()} to reorder them. Click on a {(phaseName || project?.phase_label || t('phaseText')).toLowerCase()} name to rename it. Each {(phaseName || project?.phase_label || t('phaseText')).toLowerCase()} can have a custom color.
|
||||
<div
|
||||
className={`p-3 rounded border transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-800 border-gray-700 text-gray-300'
|
||||
: 'bg-blue-50 border-blue-200 text-blue-700'
|
||||
}`}
|
||||
>
|
||||
<Text className={`text-xs font-medium ${isDarkMode ? 'text-gray-300' : 'text-blue-700'}`}>
|
||||
🎨 Drag {(phaseName || project?.phase_label || t('phasesText')).toLowerCase()} to
|
||||
reorder them. Click on a{' '}
|
||||
{(phaseName || project?.phase_label || t('phaseText')).toLowerCase()} name to rename it.
|
||||
Each {(phaseName || project?.phase_label || t('phaseText')).toLowerCase()} can have a
|
||||
custom color.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Add New Phase Form */}
|
||||
{showAddForm && (
|
||||
<div className={`p-2 rounded border-2 border-dashed transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'border-gray-600 bg-gray-700 hover:border-gray-500'
|
||||
: 'border-gray-300 bg-white hover:border-gray-400'
|
||||
} shadow-sm`}>
|
||||
<div
|
||||
className={`p-2 rounded border-2 border-dashed transition-all duration-200 ${
|
||||
isDarkMode
|
||||
? 'border-gray-600 bg-gray-700 hover:border-gray-500'
|
||||
: 'border-gray-300 bg-white hover:border-gray-400'
|
||||
} shadow-sm`}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('enterNewPhaseName')}
|
||||
value={newPhaseName}
|
||||
onChange={(e) => setNewPhaseName(e.target.value)}
|
||||
onChange={e => setNewPhaseName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`flex-1 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-600 border-gray-500 text-gray-100 placeholder-gray-400'
|
||||
isDarkMode
|
||||
? 'bg-gray-600 border-gray-500 text-gray-100 placeholder-gray-400'
|
||||
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500'
|
||||
}`}
|
||||
size="small"
|
||||
@@ -538,8 +571,8 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
}}
|
||||
size="small"
|
||||
className={`text-xs ${
|
||||
isDarkMode
|
||||
? 'text-gray-300 hover:text-gray-200 border-gray-600'
|
||||
isDarkMode
|
||||
? 'text-gray-300 hover:text-gray-200 border-gray-600'
|
||||
: 'text-gray-600 hover:text-gray-800 border-gray-300'
|
||||
}`}
|
||||
>
|
||||
@@ -551,20 +584,22 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
|
||||
{/* Add Phase Button */}
|
||||
{!showAddForm && (
|
||||
<div className={`p-3 rounded border-2 border-dashed transition-colors ${
|
||||
isDarkMode
|
||||
? 'border-gray-600 bg-gray-800/50 hover:border-gray-500'
|
||||
: 'border-gray-300 bg-gray-50/50 hover:border-gray-400'
|
||||
}`}>
|
||||
<div
|
||||
className={`p-3 rounded border-2 border-dashed transition-colors ${
|
||||
isDarkMode
|
||||
? 'border-gray-600 bg-gray-800/50 hover:border-gray-500'
|
||||
: 'border-gray-300 bg-gray-50/50 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className={`text-xs font-medium ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-700'
|
||||
}`}>
|
||||
<Text
|
||||
className={`text-xs font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
|
||||
>
|
||||
{phaseName || project?.phase_label || t('phasesText')} {t('optionsText')}
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowAddForm(true)}
|
||||
disabled={loadingPhases}
|
||||
size="small"
|
||||
@@ -583,7 +618,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{phaseList.map((phase) => (
|
||||
{phaseList.map(phase => (
|
||||
<SortablePhaseItem
|
||||
key={phase.id}
|
||||
id={phase.id}
|
||||
@@ -599,11 +634,14 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
</DndContext>
|
||||
|
||||
{phaseList.length === 0 && (
|
||||
<div className={`text-center py-8 transition-colors ${
|
||||
isDarkMode ? 'text-gray-400' : 'text-gray-500'
|
||||
}`}>
|
||||
<div
|
||||
className={`text-center py-8 transition-colors ${
|
||||
isDarkMode ? 'text-gray-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<Text className="text-sm font-medium">
|
||||
{t('no')} {(phaseName || project?.phase_label || t('phasesText')).toLowerCase()} {t('found')}
|
||||
{t('no')} {(phaseName || project?.phase_label || t('phasesText')).toLowerCase()}{' '}
|
||||
{t('found')}
|
||||
</Text>
|
||||
<br />
|
||||
<Button
|
||||
@@ -611,8 +649,8 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
size="small"
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className={`text-xs mt-1 font-medium ${
|
||||
isDarkMode
|
||||
? 'text-blue-400 hover:text-blue-300'
|
||||
isDarkMode
|
||||
? 'text-blue-400 hover:text-blue-300'
|
||||
: 'text-blue-600 hover:text-blue-700'
|
||||
}`}
|
||||
>
|
||||
@@ -625,4 +663,4 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePhaseModal;
|
||||
export default ManagePhaseModal;
|
||||
|
||||
@@ -292,7 +292,9 @@
|
||||
|
||||
/* Sortable item styling */
|
||||
.sortable-status-item {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sortable-status-item.is-dragging {
|
||||
@@ -311,11 +313,11 @@
|
||||
width: 95% !important;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
.dark-modal .ant-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.status-item {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -340,7 +342,9 @@
|
||||
.status-item-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
.status-item-exit {
|
||||
@@ -351,5 +355,7 @@
|
||||
.status-item-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,10 @@ import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
||||
import { toggleField, syncFieldWithDatabase } from '@/features/task-management/taskListFields.slice';
|
||||
import {
|
||||
toggleField,
|
||||
syncFieldWithDatabase,
|
||||
} from '@/features/task-management/taskListFields.slice';
|
||||
import { selectColumns } from '@/features/task-management/task-management.slice';
|
||||
|
||||
// Import Redux actions
|
||||
@@ -84,7 +87,7 @@ const FILTER_DEBOUNCE_DELAY = 300; // ms
|
||||
const SEARCH_DEBOUNCE_DELAY = 500; // ms
|
||||
const MAX_FILTER_OPTIONS = 100;
|
||||
|
||||
// Limit options to prevent UI lag
|
||||
// Limit options to prevent UI lag
|
||||
|
||||
// Optimized selectors with proper transformation logic
|
||||
const selectFilterData = createSelector(
|
||||
@@ -452,7 +455,7 @@ const FilterDropdown: React.FC<{
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`
|
||||
className={`
|
||||
inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md
|
||||
border transition-all duration-200 ease-in-out
|
||||
${
|
||||
@@ -689,19 +692,19 @@ const SearchFilter: React.FC<{
|
||||
value={localValue}
|
||||
onChange={e => setLocalValue(e.target.value)}
|
||||
placeholder={placeholder || t('searchTasks') || 'Search tasks by name or key...'}
|
||||
className={`w-full pr-4 pl-8 py-1 rounded border focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors duration-150 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-700 text-gray-100 placeholder-gray-400 border-gray-600'
|
||||
: 'bg-white text-gray-900 placeholder-gray-400 border-gray-300'
|
||||
}`}
|
||||
className={`w-full pr-4 pl-8 py-1 rounded border focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors duration-150 ${
|
||||
isDarkMode
|
||||
? 'bg-gray-700 text-gray-100 placeholder-gray-400 border-gray-600'
|
||||
: 'bg-white text-gray-900 placeholder-gray-400 border-gray-300'
|
||||
}`}
|
||||
/>
|
||||
{localValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className={`absolute right-1.5 top-1/2 transform -translate-y-1/2 transition-colors duration-150 ${
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200'
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
@@ -716,9 +719,9 @@ const SearchFilter: React.FC<{
|
||||
? 'text-white bg-gray-600 hover:bg-gray-700'
|
||||
: 'text-gray-800 bg-gray-200 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{t('search')}
|
||||
</button>
|
||||
>
|
||||
{t('search')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -727,9 +730,7 @@ const SearchFilter: React.FC<{
|
||||
setIsExpanded(false);
|
||||
}}
|
||||
className={`px-2.5 py-1.5 text-xs font-medium transition-colors duration-200 ${
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
isDarkMode ? 'text-gray-400 hover:text-gray-200' : 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{t('cancel')}
|
||||
@@ -751,30 +752,33 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Helper function to get translated field label using existing task-list-table translations
|
||||
const getFieldLabel = useCallback((fieldKey: string) => {
|
||||
const keyMappings: Record<string, string> = {
|
||||
'KEY': 'keyColumn',
|
||||
'DESCRIPTION': 'descriptionColumn',
|
||||
'PROGRESS': 'progressColumn',
|
||||
'ASSIGNEES': 'assigneesColumn',
|
||||
'LABELS': 'labelsColumn',
|
||||
'PHASE': 'phaseColumn',
|
||||
'STATUS': 'statusColumn',
|
||||
'PRIORITY': 'priorityColumn',
|
||||
'TIME_TRACKING': 'timeTrackingColumn',
|
||||
'ESTIMATION': 'estimationColumn',
|
||||
'START_DATE': 'startDateColumn',
|
||||
'DUE_DATE': 'dueDateColumn',
|
||||
'DUE_TIME': 'dueTimeColumn',
|
||||
'COMPLETED_DATE': 'completedDateColumn',
|
||||
'CREATED_DATE': 'createdDateColumn',
|
||||
'LAST_UPDATED': 'lastUpdatedColumn',
|
||||
'REPORTER': 'reporterColumn',
|
||||
};
|
||||
const getFieldLabel = useCallback(
|
||||
(fieldKey: string) => {
|
||||
const keyMappings: Record<string, string> = {
|
||||
KEY: 'keyColumn',
|
||||
DESCRIPTION: 'descriptionColumn',
|
||||
PROGRESS: 'progressColumn',
|
||||
ASSIGNEES: 'assigneesColumn',
|
||||
LABELS: 'labelsColumn',
|
||||
PHASE: 'phaseColumn',
|
||||
STATUS: 'statusColumn',
|
||||
PRIORITY: 'priorityColumn',
|
||||
TIME_TRACKING: 'timeTrackingColumn',
|
||||
ESTIMATION: 'estimationColumn',
|
||||
START_DATE: 'startDateColumn',
|
||||
DUE_DATE: 'dueDateColumn',
|
||||
DUE_TIME: 'dueTimeColumn',
|
||||
COMPLETED_DATE: 'completedDateColumn',
|
||||
CREATED_DATE: 'createdDateColumn',
|
||||
LAST_UPDATED: 'lastUpdatedColumn',
|
||||
REPORTER: 'reporterColumn',
|
||||
};
|
||||
|
||||
const translationKey = keyMappings[fieldKey];
|
||||
return translationKey ? tTable(translationKey) : fieldKey;
|
||||
}, [tTable]);
|
||||
const translationKey = keyMappings[fieldKey];
|
||||
return translationKey ? tTable(translationKey) : fieldKey;
|
||||
},
|
||||
[tTable]
|
||||
);
|
||||
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
||||
const columns = useSelector(selectColumns);
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
@@ -859,9 +863,9 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
||||
{/* Options List */}
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{sortedFields.length === 0 ? (
|
||||
<div className={`p-2 text-xs text-center ${themeClasses.secondaryText}`}>
|
||||
{t('noOptionsFound')}
|
||||
</div>
|
||||
<div className={`p-2 text-xs text-center ${themeClasses.secondaryText}`}>
|
||||
{t('noOptionsFound')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-0.5">
|
||||
{sortedFields.map(field => {
|
||||
@@ -873,15 +877,17 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
||||
onClick={() => {
|
||||
// Toggle field locally first
|
||||
dispatch(toggleField(field.key));
|
||||
|
||||
|
||||
// Sync with database if projectId is available
|
||||
if (projectId) {
|
||||
dispatch(syncFieldWithDatabase({
|
||||
projectId,
|
||||
fieldKey: field.key,
|
||||
visible: !field.visible,
|
||||
columns
|
||||
}));
|
||||
dispatch(
|
||||
syncFieldWithDatabase({
|
||||
projectId,
|
||||
fieldKey: field.key,
|
||||
visible: !field.visible,
|
||||
columns,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
@@ -949,7 +955,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
// Get search value from Redux based on position
|
||||
const taskManagementSearch = useAppSelector(state => state.taskManagement?.search || '');
|
||||
const kanbanSearch = useAppSelector(state => state.enhancedKanbanReducer?.search || '');
|
||||
|
||||
|
||||
const searchValue = position === 'board' ? kanbanSearch : taskManagementSearch;
|
||||
|
||||
// Local state for filter sections
|
||||
@@ -957,7 +963,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
|
||||
const [clearingFilters, setClearingFilters] = useState(false);
|
||||
|
||||
|
||||
// Modal state
|
||||
const [showManageStatusModal, setShowManageStatusModal] = useState(false);
|
||||
const [showManagePhaseModal, setShowManagePhaseModal] = useState(false);
|
||||
@@ -1306,12 +1312,12 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
))
|
||||
) : (
|
||||
// Loading state
|
||||
<div
|
||||
className={`flex items-center gap-2 px-2.5 py-1.5 text-xs ${themeClasses.secondaryText}`}
|
||||
>
|
||||
<div className="animate-spin rounded-full h-3.5 w-3.5 border-b-2 border-gray-500"></div>
|
||||
<span>{t('loadingFilters')}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-2 px-2.5 py-1.5 text-xs ${themeClasses.secondaryText}`}
|
||||
>
|
||||
<div className="animate-spin rounded-full h-3.5 w-3.5 border-b-2 border-gray-500"></div>
|
||||
<span>{t('loadingFilters')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1321,17 +1327,18 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
{activeFiltersCount > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`text-xs ${themeClasses.secondaryText}`}>
|
||||
{activeFiltersCount} {activeFiltersCount !== 1 ? t('filtersActive') : t('filterActive')}
|
||||
{activeFiltersCount}{' '}
|
||||
{activeFiltersCount !== 1 ? t('filtersActive') : t('filterActive')}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
disabled={clearingFilters}
|
||||
className={`text-xs font-medium transition-colors duration-150 ${
|
||||
clearingFilters
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300'
|
||||
: 'text-gray-600 hover:text-gray-700'
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300'
|
||||
: 'text-gray-600 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{clearingFilters ? t('clearing') : t('clearAll')}
|
||||
@@ -1362,7 +1369,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Modals */}
|
||||
<ManageStatusModal
|
||||
open={showManageStatusModal}
|
||||
@@ -1373,7 +1380,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
}}
|
||||
projectId={projectId || undefined}
|
||||
/>
|
||||
|
||||
|
||||
<ManagePhaseModal
|
||||
open={showManagePhaseModal}
|
||||
onClose={() => {
|
||||
|
||||
@@ -76,7 +76,12 @@ const LazyAssigneeSelectorWrapper: React.FC<LazyAssigneeSelectorProps> = ({
|
||||
// Once loaded, show the full component
|
||||
return (
|
||||
<Suspense fallback={<LoadingPlaceholder isDarkMode={isDarkMode} />}>
|
||||
<LazyAssigneeSelector task={task} groupId={groupId} isDarkMode={isDarkMode} kanbanMode={kanbanMode} />
|
||||
<LazyAssigneeSelector
|
||||
task={task}
|
||||
groupId={groupId}
|
||||
isDarkMode={isDarkMode}
|
||||
kanbanMode={kanbanMode}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,8 +100,13 @@ export const LazyTaskRowWithSubtasks = createOptimizedLazy(
|
||||
);
|
||||
|
||||
export const LazyCustomColumnModal = createOptimizedLazy(
|
||||
() => import('@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal'),
|
||||
<div className="p-4"><Skeleton active /></div>
|
||||
() =>
|
||||
import(
|
||||
'@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal'
|
||||
),
|
||||
<div className="p-4">
|
||||
<Skeleton active />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const LazyLabelsSelector = createOptimizedLazy(
|
||||
@@ -141,17 +146,13 @@ export const ProgressiveEnhancement: React.FC<ProgressiveEnhancementProps> = ({
|
||||
condition,
|
||||
children,
|
||||
fallback,
|
||||
loadingComponent = <Skeleton active />
|
||||
loadingComponent = <Skeleton active />,
|
||||
}) => {
|
||||
if (!condition) {
|
||||
return <>{fallback || loadingComponent}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={loadingComponent}>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
return <Suspense fallback={loadingComponent}>{children}</Suspense>;
|
||||
};
|
||||
|
||||
// Intersection observer based lazy loading for components
|
||||
@@ -168,7 +169,7 @@ export const IntersectionLazyLoad: React.FC<IntersectionLazyLoadProps> = ({
|
||||
fallback = <Skeleton active />,
|
||||
rootMargin = '100px',
|
||||
threshold = 0.1,
|
||||
once = true
|
||||
once = true,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = React.useState(false);
|
||||
const [hasBeenVisible, setHasBeenVisible] = React.useState(false);
|
||||
@@ -204,13 +205,7 @@ export const IntersectionLazyLoad: React.FC<IntersectionLazyLoadProps> = ({
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{shouldRender ? (
|
||||
<Suspense fallback={fallback}>
|
||||
{children}
|
||||
</Suspense>
|
||||
) : (
|
||||
fallback
|
||||
)}
|
||||
{shouldRender ? <Suspense fallback={fallback}>{children}</Suspense> : fallback}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -221,7 +216,7 @@ export const createRouteComponent = <T extends ComponentType<any>>(
|
||||
pageTitle?: string
|
||||
) => {
|
||||
const LazyComponent = createOptimizedLazy(importFunc);
|
||||
|
||||
|
||||
return React.memo(() => {
|
||||
React.useEffect(() => {
|
||||
if (pageTitle) {
|
||||
@@ -229,17 +224,17 @@ export const createRouteComponent = <T extends ComponentType<any>>(
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Spin size="large" tip={`Loading ${pageTitle || 'page'}...`} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LazyComponent {...({} as any)} />
|
||||
</Suspense>
|
||||
);
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Spin size="large" tip={`Loading ${pageTitle || 'page'}...`} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LazyComponent {...({} as any)} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -365,7 +360,7 @@ export const LazyLoadingExamples = {
|
||||
// Progressive enhancement
|
||||
ProgressiveExample: () => (
|
||||
<ProgressiveEnhancement condition={true}>
|
||||
<LazyTaskRow
|
||||
<LazyTaskRow
|
||||
task={{ id: '1', status: 'todo', priority: 'medium', created_at: '', updated_at: '' }}
|
||||
projectId="123"
|
||||
groupId="group1"
|
||||
@@ -388,7 +383,7 @@ export const LazyLoadingExamples = {
|
||||
ErrorBoundaryExample: () => (
|
||||
<LazyErrorBoundary>
|
||||
<Suspense fallback={<Skeleton active />}>
|
||||
<LazyTaskRow
|
||||
<LazyTaskRow
|
||||
task={{ id: '1', status: 'todo', priority: 'medium', created_at: '', updated_at: '' }}
|
||||
projectId="123"
|
||||
groupId="group1"
|
||||
@@ -400,4 +395,4 @@ export const LazyLoadingExamples = {
|
||||
</Suspense>
|
||||
</LazyErrorBoundary>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button, Typography, Dropdown, Popconfirm, Tooltip, Space, Badge, Divider } from '@/shared/antd-imports';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Dropdown,
|
||||
Popconfirm,
|
||||
Tooltip,
|
||||
Space,
|
||||
Badge,
|
||||
Divider,
|
||||
} from '@/shared/antd-imports';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
CloseOutlined,
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Button, Table, Progress, Alert, Space, Typography, Divider } from '@/shared/antd-imports';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Table,
|
||||
Progress,
|
||||
Alert,
|
||||
Space,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@/shared/antd-imports';
|
||||
import { performanceMonitor } from '@/utils/performance-monitor';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -11,13 +11,19 @@ import {
|
||||
DownOutlined,
|
||||
} from '@/shared/antd-imports';
|
||||
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
||||
import { taskManagementSelectors, selectAllTasks } from '@/features/task-management/task-management.slice';
|
||||
import {
|
||||
taskManagementSelectors,
|
||||
selectAllTasks,
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import { RootState } from '@/app/store';
|
||||
import TaskRow from './task-row';
|
||||
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||
import { TaskListField } from '@/features/task-management/taskListFields.slice';
|
||||
import { Checkbox } from '@/components';
|
||||
import { selectIsGroupCollapsed, toggleGroupCollapsed } from '@/features/task-management/grouping.slice';
|
||||
import {
|
||||
selectIsGroupCollapsed,
|
||||
toggleGroupCollapsed,
|
||||
} from '@/features/task-management/grouping.slice';
|
||||
import { selectIsTaskSelected } from '@/features/task-management/selection.slice';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ import {
|
||||
selectTaskGroupsV3,
|
||||
fetchSubTasks,
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import {
|
||||
selectCurrentGrouping,
|
||||
} from '@/features/task-management/grouping.slice';
|
||||
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||
import {
|
||||
selectSelectedTaskIds,
|
||||
clearSelection,
|
||||
@@ -244,7 +242,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
|
||||
// Measure task loading performance
|
||||
CustomPerformanceMeasurer.mark('task-load-time');
|
||||
|
||||
|
||||
// Fetch real tasks from V3 API (minimal processing needed)
|
||||
dispatch(fetchTasksV3(projectId)).finally(() => {
|
||||
CustomPerformanceMeasurer.measure('task-load-time');
|
||||
@@ -270,7 +268,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const taskId = active.id as string;
|
||||
|
||||
// Find the task and its group
|
||||
const activeTask = Array.isArray(tasks) ? tasks.find((t: Task) => t.id === taskId) || null : null;
|
||||
const activeTask = Array.isArray(tasks)
|
||||
? tasks.find((t: Task) => t.id === taskId) || null
|
||||
: null;
|
||||
let activeGroupId: string | null = null;
|
||||
|
||||
if (activeTask) {
|
||||
@@ -302,7 +302,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const overId = over.id as string;
|
||||
|
||||
// Check if we're hovering over a task or a group container
|
||||
const targetTask = Array.isArray(tasks) ? tasks.find((t: Task) => t.id === overId) : undefined;
|
||||
const targetTask = Array.isArray(tasks)
|
||||
? tasks.find((t: Task) => t.id === overId)
|
||||
: undefined;
|
||||
let targetGroupId = overId;
|
||||
|
||||
if (targetTask) {
|
||||
@@ -352,7 +354,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
let targetIndex = -1;
|
||||
|
||||
// Check if dropping on a task or a group
|
||||
const targetTask = Array.isArray(tasks) ? tasks.find((t: Task) => t.id === overId) : undefined;
|
||||
const targetTask = Array.isArray(tasks)
|
||||
? tasks.find((t: Task) => t.id === overId)
|
||||
: undefined;
|
||||
if (targetTask) {
|
||||
// Dropping on a task, find which group contains this task
|
||||
for (const group of taskGroups) {
|
||||
@@ -435,40 +439,42 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const newSelectedIds = Array.from(currentSelectedIds);
|
||||
|
||||
// Map selected tasks to the required format
|
||||
const newSelectedTasks = Array.isArray(tasks) ? tasks
|
||||
.filter((t: Task) => newSelectedIds.includes(t.id))
|
||||
.map(
|
||||
(task: Task): IProjectTask => ({
|
||||
id: task.id,
|
||||
name: task.title,
|
||||
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 as any).createdAt || (task as any).created_at,
|
||||
updated_at: (task as any).updatedAt || (task as any).updated_at,
|
||||
sort_order: task.order,
|
||||
})
|
||||
) : [];
|
||||
const newSelectedTasks = Array.isArray(tasks)
|
||||
? tasks
|
||||
.filter((t: Task) => newSelectedIds.includes(t.id))
|
||||
.map(
|
||||
(task: Task): IProjectTask => ({
|
||||
id: task.id,
|
||||
name: task.title,
|
||||
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 as any).createdAt || (task as any).created_at,
|
||||
updated_at: (task as any).updatedAt || (task as any).updated_at,
|
||||
sort_order: task.order,
|
||||
})
|
||||
)
|
||||
: [];
|
||||
|
||||
// Dispatch both actions to update the Redux state
|
||||
dispatch(selectTasks(newSelectedTasks));
|
||||
|
||||
@@ -51,4 +51,4 @@ const TaskListFilters: React.FC<TaskListFiltersProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListFilters;
|
||||
export default TaskListFilters;
|
||||
|
||||
@@ -48,16 +48,10 @@ const TaskListGroup: React.FC<TaskListGroupProps> = ({
|
||||
{!isCollapsed && (
|
||||
<div className="task-list">
|
||||
{tasks.map((task, index) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: task.id,
|
||||
});
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
useSortable({
|
||||
id: task.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -91,4 +85,4 @@ const TaskListGroup: React.FC<TaskListGroupProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListGroup;
|
||||
export default TaskListGroup;
|
||||
|
||||
@@ -5,23 +5,14 @@ interface TaskListHeaderProps {
|
||||
onCollapseAll: () => void;
|
||||
}
|
||||
|
||||
const TaskListHeader: React.FC<TaskListHeaderProps> = ({
|
||||
onExpandAll,
|
||||
onCollapseAll,
|
||||
}) => {
|
||||
const TaskListHeader: React.FC<TaskListHeaderProps> = ({ onExpandAll, onCollapseAll }) => {
|
||||
return (
|
||||
<div className="task-list-header">
|
||||
<div className="header-actions">
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={onExpandAll}
|
||||
>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onExpandAll}>
|
||||
Expand All
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm ml-2"
|
||||
onClick={onCollapseAll}
|
||||
>
|
||||
<button className="btn btn-secondary btn-sm ml-2" onClick={onCollapseAll}>
|
||||
Collapse All
|
||||
</button>
|
||||
</div>
|
||||
@@ -29,4 +20,4 @@ const TaskListHeader: React.FC<TaskListHeaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListHeader;
|
||||
export default TaskListHeader;
|
||||
|
||||
@@ -70,11 +70,11 @@ const TaskPriorityDropdown: React.FC<TaskPriorityDropdownProps> = ({
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const dropdownHeight = 200; // Estimated dropdown height
|
||||
|
||||
|
||||
// Check if dropdown would go below viewport
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
||||
|
||||
|
||||
setDropdownPosition({
|
||||
top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
|
||||
left: rect.left,
|
||||
|
||||
@@ -1017,7 +1017,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
||||
<div className="task-indicators flex items-center gap-2">
|
||||
{/* Comments indicator */}
|
||||
{(task as any).comments_count > 0 && (
|
||||
<Tooltip title={t(`task-management:indicators.tooltips.comments${(task as any).comments_count === 1 ? '' : '_plural'}`, { count: (task as any).comments_count })}>
|
||||
<Tooltip
|
||||
title={t(
|
||||
`task-management:indicators.tooltips.comments${(task as any).comments_count === 1 ? '' : '_plural'}`,
|
||||
{ count: (task as any).comments_count }
|
||||
)}
|
||||
>
|
||||
<MessageOutlined
|
||||
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
|
||||
/>
|
||||
@@ -1025,7 +1030,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
||||
)}
|
||||
{/* Attachments indicator */}
|
||||
{(task as any).attachments_count > 0 && (
|
||||
<Tooltip title={t(`task-management:indicators.tooltips.attachments${(task as any).attachments_count === 1 ? '' : '_plural'}`, { count: (task as any).attachments_count })}>
|
||||
<Tooltip
|
||||
title={t(
|
||||
`task-management:indicators.tooltips.attachments${(task as any).attachments_count === 1 ? '' : '_plural'}`,
|
||||
{ count: (task as any).attachments_count }
|
||||
)}
|
||||
>
|
||||
<PaperClipOutlined
|
||||
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
|
||||
/>
|
||||
@@ -1336,8 +1346,8 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
||||
handleDateChange(null, 'startDate');
|
||||
}}
|
||||
className={`absolute right-1 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
title="Clear start date"
|
||||
@@ -1375,8 +1385,8 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
||||
handleDateChange(null, 'dueDate');
|
||||
}}
|
||||
className={`absolute right-1 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
title="Clear due date"
|
||||
@@ -1638,4 +1648,4 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
|
||||
|
||||
TaskRow.displayName = 'TaskRow';
|
||||
|
||||
export default TaskRow;
|
||||
export default TaskRow;
|
||||
|
||||
@@ -77,11 +77,11 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const dropdownHeight = 200; // Estimated dropdown height
|
||||
|
||||
|
||||
// Check if dropdown would go below viewport
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
||||
|
||||
|
||||
setDropdownPosition({
|
||||
top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
|
||||
left: rect.left,
|
||||
|
||||
Reference in New Issue
Block a user