feat(task-management): enhance task grouping and localization support

- Implemented unmapped task grouping for better organization of tasks without valid phases.
- Updated task distribution logic to handle unmapped tasks and added a corresponding group in the response.
- Enhanced localization by adding translations for "noTasksInGroup" in multiple languages.
- Improved task list components to support custom columns and better task management features.
- Refactored task management slice to include loading states for columns and custom columns.
This commit is contained in:
chamikaJ
2025-07-04 20:41:03 +05:30
parent 9e29031703
commit f30fde553d
23 changed files with 1560 additions and 380 deletions

View File

@@ -5,217 +5,26 @@
width: 8px;
}
/* Enhanced Project View Tab Styles - Compact */
.project-view-tabs {
margin-top: 16px;
/* Light mode - selected tab header bold */
[data-theme="light"] .project-view-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
font-weight: 700;
color: #000000 !important;
}
/* Remove default tab border */
.project-view-tabs .ant-tabs-nav::before {
border: none !important;
/* Dark mode - selected tab header bold and white */
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
font-weight: 900;
color: #ffffff;
}
/* Tab bar container */
.project-view-tabs .ant-tabs-nav {
margin-bottom: 8px;
background: transparent;
padding: 0 12px;
/* Light mode - selected tab underline black */
[data-theme="light"] .project-view-tabs .ant-tabs-ink-bar {
background-color: #000000 !important;
}
/* Individual tab styling - Compact */
.project-view-tabs .ant-tabs-tab {
position: relative;
margin: 0 4px 0 0;
padding: 8px 16px;
border-radius: 6px 6px 0 0;
background: transparent;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
font-size: 13px;
min-height: 36px;
display: flex;
align-items: center;
/* Dark mode - selected tab underline white */
[data-theme="dark"] .project-view-tabs .ant-tabs-ink-bar {
background-color: #ffffff;
}
/* Light mode tab styles */
[data-theme="default"] .project-view-tabs .ant-tabs-tab {
color: #64748b;
background: #f8fafc;
}
[data-theme="default"] .project-view-tabs .ant-tabs-tab:hover {
color: #3b82f6;
background: #eff6ff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
[data-theme="default"] .project-view-tabs .ant-tabs-tab-active {
color: #1e40af !important;
background: #ffffff !important;
box-shadow:
0 -2px 8px rgba(59, 130, 246, 0.1),
0 4px 16px rgba(59, 130, 246, 0.1);
z-index: 1;
}
/* Dark mode tab styles - matching task list row colors */
[data-theme="dark"] .project-view-tabs .ant-tabs-tab {
color: #94a3b8;
background: #141414;
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab:hover {
color: #60a5fa;
background: #262626;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(96, 165, 250, 0.2);
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active {
color: #60a5fa !important;
background: #1f1f1f !important;
box-shadow:
0 -2px 8px rgba(96, 165, 250, 0.15),
0 4px 16px rgba(96, 165, 250, 0.15);
z-index: 1;
}
/* Tab content area - Compact */
.project-view-tabs .ant-tabs-content-holder {
background: transparent;
border-radius: 6px;
position: relative;
z-index: 0;
margin-top: 4px;
}
[data-theme="default"] .project-view-tabs .ant-tabs-content-holder {
background: #ffffff;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] .project-view-tabs .ant-tabs-content-holder {
background: #1f1f1f;
border: 1px solid #303030;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.project-view-tabs .ant-tabs-tabpane {
padding: 0;
min-height: 300px;
}
/* Pin button styling - Compact */
.project-view-tabs .borderless-icon-btn {
margin-left: 6px;
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
opacity: 0.7;
}
.project-view-tabs .borderless-icon-btn:hover {
opacity: 1;
transform: scale(1.05);
}
[data-theme="default"] .project-view-tabs .borderless-icon-btn:hover {
background: rgba(59, 130, 246, 0.1) !important;
}
[data-theme="dark"] .project-view-tabs .borderless-icon-btn:hover {
background: rgba(96, 165, 250, 0.1) !important;
}
/* Pinned tab indicator */
.project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
opacity: 1;
}
[data-theme="default"] .project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
background: rgba(59, 130, 246, 0.1) !important;
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
background: rgba(96, 165, 250, 0.1) !important;
}
/* Tab label flex container */
.project-view-tabs .ant-tabs-tab .ant-tabs-tab-btn {
display: flex;
align-items: center;
width: 100%;
}
/* Responsive adjustments - Compact */
@media (max-width: 768px) {
.project-view-tabs .ant-tabs-nav {
padding: 0 8px;
}
.project-view-tabs .ant-tabs-tab {
margin: 0 2px 0 0;
padding: 6px 12px;
font-size: 12px;
min-height: 32px;
}
.project-view-tabs .borderless-icon-btn {
margin-left: 4px;
padding: 1px;
}
}
@media (max-width: 480px) {
.project-view-tabs .ant-tabs-tab {
padding: 6px 10px;
font-size: 11px;
min-height: 30px;
}
.project-view-tabs .borderless-icon-btn {
display: none; /* Hide pin buttons on very small screens */
}
}
/* Animation for tab switching */
.project-view-tabs .ant-tabs-content {
position: relative;
}
.project-view-tabs .ant-tabs-tabpane-active {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Focus states for accessibility - Compact */
.project-view-tabs .ant-tabs-tab:focus-visible {
outline: 1px solid #3b82f6;
outline-offset: 1px;
border-radius: 6px;
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab:focus-visible {
outline-color: #60a5fa;
}
/* Loading state for tab content */
.project-view-tabs .ant-tabs-tabpane .suspense-fallback {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}

View File

@@ -351,20 +351,12 @@ const ProjectView = React.memo(() => {
activeKey={activeTab}
onChange={handleTabChange}
items={tabMenuItems}
tabBarStyle={{
paddingInline: 0,
marginBottom: 8,
background: 'transparent',
minHeight: '36px',
}}
tabBarGutter={0}
destroyInactiveTabPane={true}
destroyOnHidden={true}
animated={{
inkBar: true,
tabPane: false,
}}
size="small"
type="card"
/>
{portalElements}

View File

@@ -10,6 +10,7 @@ import {
Typography,
Popconfirm,
} from 'antd';
import { useTranslation } from 'react-i18next';
import SelectionTypeColumn from './selection-type-column/selection-type-column';
import NumberTypeColumn from './number-type-column/number-type-column';
import LabelTypeColumn from './label-type-column/label-type-column';
@@ -31,6 +32,7 @@ import {
setSecondNumericColumn,
setSelectionsList,
setLabelsList,
resetCustomFieldValues,
} from '@features/projects/singleProject/task-list-custom-columns/task-list-custom-columns-slice';
import CustomColumnHeader from '../custom-column-header/custom-column-header';
import { nanoid } from '@reduxjs/toolkit';
@@ -41,10 +43,12 @@ import {
import { themeWiseColor } from '@/utils/themeWiseColor';
import KeyTypeColumn from './key-type-column/key-type-column';
import logger from '@/utils/errorLogger';
import {
import {
fetchTasksV3,
fetchTaskListColumns,
addCustomColumn,
deleteCustomColumn as deleteCustomColumnFromTasks,
} from '@/features/tasks/tasks.slice';
deleteCustomColumn as deleteCustomColumnFromTaskManagement,
} from '@/features/task-management/task-management.slice';
import { useParams } from 'react-router-dom';
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
import { ExclamationCircleFilled } from '@ant-design/icons';
@@ -52,6 +56,7 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
const CustomColumnModal = () => {
const [mainForm] = Form.useForm();
const { projectId } = useParams();
const { t } = useTranslation('task-list-table');
// get theme details from theme reducer
const themeMode = useAppSelector(state => state.themeReducer.mode);
@@ -62,6 +67,7 @@ const CustomColumnModal = () => {
customColumnId,
customColumnModalType,
isCustomColumnModalOpen,
currentColumnData,
decimals,
label,
labelPosition,
@@ -82,35 +88,84 @@ const CustomColumnModal = () => {
state => state.taskListCustomColumnsReducer.customFieldNumberType
);
// if it is already created column get the column data
const openedColumn = useAppSelector(state => state.taskReducer.customColumns).find(
col => col.id === customColumnId
);
// Use the column data passed from TaskListV2
const openedColumn = currentColumnData;
// Debug logging
console.log('Modal Debug Info:', {
customColumnId,
customColumnModalType,
currentColumnData,
openedColumn,
openedColumnFound: !!openedColumn,
openedColumnId: openedColumn?.id
});
// Function to reset all form and Redux state
const resetModalData = () => {
mainForm.resetFields();
dispatch(resetCustomFieldValues());
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
};
// Function to handle deleting a custom column
const handleDeleteColumn = async () => {
if (!customColumnId) return;
console.log('Delete function called with:', {
customColumnId,
openedColumn,
openedColumnId: openedColumn?.id,
openedColumnKey: openedColumn?.key,
fullColumnData: openedColumn
});
// Try to get UUID from different possible locations in the column data
const columnUUID = openedColumn?.id ||
openedColumn?.uuid ||
openedColumn?.custom_column_obj?.id ||
openedColumn?.custom_column_obj?.uuid;
console.log('Extracted UUID candidates:', {
'openedColumn?.id': openedColumn?.id,
'openedColumn?.uuid': openedColumn?.uuid,
'openedColumn?.custom_column_obj?.id': openedColumn?.custom_column_obj?.id,
'openedColumn?.custom_column_obj?.uuid': openedColumn?.custom_column_obj?.uuid,
'finalColumnUUID': columnUUID
});
if (!customColumnId || !columnUUID) {
console.error('Missing required data for deletion:', {
customColumnId,
columnUUID,
openedColumn
});
message.error('Cannot delete column: Missing UUID');
return;
}
try {
console.log('Attempting to delete column with UUID:', columnUUID);
// Make API request to delete the custom column using the service
await tasksCustomColumnsService.deleteCustomColumn(openedColumn?.id || customColumnId);
await tasksCustomColumnsService.deleteCustomColumn(columnUUID);
// Dispatch actions to update the Redux store
dispatch(deleteCustomColumnFromTasks(customColumnId));
dispatch(deleteCustomColumnFromTaskManagement(customColumnId));
dispatch(deleteCustomColumnFromColumns(customColumnId));
// Close the modal
// Close the modal and reset data
dispatch(toggleCustomColumnModalOpen(false));
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
resetModalData();
// Show success message
message.success('Custom column deleted successfully');
message.success(t('customColumns.modal.deleteSuccessMessage'));
// Reload the page to reflect the changes
window.location.reload();
// Refresh tasks and columns to reflect the deleted custom column
if (projectId) {
dispatch(fetchTaskListColumns(projectId));
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error deleting custom column:', error);
message.error('Failed to delete custom column');
message.error(t('customColumns.modal.deleteErrorMessage'));
}
};
@@ -118,49 +173,49 @@ const CustomColumnModal = () => {
{
key: 'people',
value: 'people',
label: 'People',
label: t('customColumns.fieldTypes.people'),
disabled: false,
},
{
key: 'number',
value: 'number',
label: 'Number',
label: t('customColumns.fieldTypes.number'),
disabled: false,
},
{
key: 'date',
value: 'date',
label: 'Date',
label: t('customColumns.fieldTypes.date'),
disabled: false,
},
{
key: 'selection',
value: 'selection',
label: 'Selection',
label: t('customColumns.fieldTypes.selection'),
disabled: false,
},
{
key: 'checkbox',
value: 'checkbox',
label: 'Checkbox',
label: t('customColumns.fieldTypes.checkbox'),
disabled: true,
},
{
key: 'labels',
value: 'labels',
label: 'Labels',
label: t('customColumns.fieldTypes.labels'),
disabled: true,
},
{
key: 'key',
value: 'key',
label: 'Key',
label: t('customColumns.fieldTypes.key'),
disabled: true,
},
{
key: 'formula',
value: 'formula',
label: 'Formula',
label: t('customColumns.fieldTypes.formula'),
disabled: true,
},
];
@@ -231,12 +286,21 @@ const CustomColumnModal = () => {
if (res.done) {
if (res.body.id) newColumn.id = res.body.id;
dispatch(addCustomColumn(newColumn));
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
dispatch(toggleCustomColumnModalOpen(false));
resetModalData();
// Show success message
message.success(t('customColumns.modal.createSuccessMessage'));
// Refresh tasks and columns to include the new custom column values
if (projectId) {
dispatch(fetchTaskListColumns(projectId));
dispatch(fetchTasksV3(projectId));
}
}
} catch (error) {
logger.error('Error creating custom column:', error);
message.error('Failed to create custom column');
message.error(t('customColumns.modal.createErrorMessage'));
}
} else if (customColumnModalType === 'edit' && customColumnId) {
const updatedColumn = openedColumn
@@ -264,7 +328,7 @@ const CustomColumnModal = () => {
}
: null;
if (updatedColumn) {
if (updatedColumn && openedColumn?.id) {
try {
// Prepare the configuration object
const configuration = {
@@ -299,7 +363,7 @@ const CustomColumnModal = () => {
};
// Make API request to update custom column using the service
await tasksCustomColumnsService.updateCustomColumn(openedColumn?.id || customColumnId, {
await tasksCustomColumnsService.updateCustomColumn(openedColumn.id, {
name: value.fieldTitle,
field_type: value.fieldType,
width: 150,
@@ -307,15 +371,21 @@ const CustomColumnModal = () => {
configuration,
});
// Close modal
// Close modal and reset data
dispatch(toggleCustomColumnModalOpen(false));
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
resetModalData();
// Reload the page instead of updating the slice
window.location.reload();
// Show success message
message.success(t('customColumns.modal.updateSuccessMessage'));
// Refresh tasks and columns to reflect the updated custom column
if (projectId) {
dispatch(fetchTaskListColumns(projectId));
dispatch(fetchTasksV3(projectId));
}
} catch (error) {
logger.error('Error updating custom column:', error);
message.error('Failed to update custom column');
message.error(t('customColumns.modal.updateErrorMessage'));
}
}
}
@@ -328,20 +398,17 @@ const CustomColumnModal = () => {
return (
<Modal
title={customColumnModalType === 'create' ? 'Add field' : 'Edit field'}
title={customColumnModalType === 'create' ? t('customColumns.modal.addFieldTitle') : t('customColumns.modal.editFieldTitle')}
centered
open={isCustomColumnModalOpen}
onCancel={() => {
dispatch(toggleCustomColumnModalOpen(false));
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
resetModalData();
}}
styles={{
header: { position: 'relative' },
footer: { display: 'none' },
}}
onClose={() => {
mainForm.resetFields();
}}
afterOpenChange={open => {
if (open && customColumnModalType === 'edit' && openedColumn) {
// Set the field type first so the correct form fields are displayed
@@ -394,9 +461,11 @@ const CustomColumnModal = () => {
secondNumericColumn: openedColumn.custom_column_obj?.secondNumericColumn,
});
} else if (open && customColumnModalType === 'create') {
// Reset form for create mode
mainForm.resetFields();
dispatch(setCustomFieldType('people'));
// Reset all data for create mode
resetModalData();
} else if (!open) {
// Reset data when modal closes
resetModalData();
}
}}
>
@@ -437,22 +506,22 @@ const CustomColumnModal = () => {
<Flex gap={16} align="center" justify="space-between">
<Form.Item
name={'fieldTitle'}
label={<Typography.Text>Field title</Typography.Text>}
label={<Typography.Text>{t('customColumns.modal.fieldTitle')}</Typography.Text>}
layout="vertical"
rules={[
{
required: true,
message: 'Field title is required',
message: t('customColumns.modal.fieldTitleRequired'),
},
]}
required={false}
>
<Input placeholder="title" style={{ minWidth: '100%', width: 300 }} />
<Input placeholder={t('customColumns.modal.columnTitlePlaceholder')} style={{ minWidth: '100%', width: 300 }} />
</Form.Item>
<Form.Item
name={'fieldType'}
label={<Typography.Text>Type</Typography.Text>}
label={<Typography.Text>{t('customColumns.modal.type')}</Typography.Text>}
layout="vertical"
>
<Select
@@ -485,27 +554,30 @@ const CustomColumnModal = () => {
>
{customColumnModalType === 'edit' && customColumnId && (
<Popconfirm
title="Are you sure you want to delete this custom column?"
description="This action cannot be undone. All data associated with this column will be permanently deleted."
title={t('customColumns.modal.deleteConfirmTitle')}
description={t('customColumns.modal.deleteConfirmDescription')}
icon={<ExclamationCircleFilled style={{ color: 'red' }} />}
onConfirm={handleDeleteColumn}
okText="Delete"
cancelText="Cancel"
okText={t('customColumns.modal.deleteButton')}
cancelText={t('customColumns.modal.cancelButton')}
okButtonProps={{ danger: true }}
>
<Button danger>Delete</Button>
<Button danger>{t('customColumns.modal.deleteButton')}</Button>
</Popconfirm>
)}
<Flex gap={8}>
<Button onClick={() => dispatch(toggleCustomColumnModalOpen(false))}>Cancel</Button>
<Button onClick={() => {
dispatch(toggleCustomColumnModalOpen(false));
resetModalData();
}}>{t('customColumns.modal.cancelButton')}</Button>
{customColumnModalType === 'create' ? (
<Button type="primary" htmlType="submit">
Create
{t('customColumns.modal.createButton')}
</Button>
) : (
<Button type="primary" htmlType="submit">
Update
{t('customColumns.modal.updateButton')}
</Button>
)}
</Flex>