feat(task-list): refine task list components and improve UI consistency
- Updated SubtaskLoadingSkeleton and TaskRow components for better spacing and visual consistency. - Simplified TaskGroupHeader by removing unnecessary elements and enhancing the display of group names. - Adjusted TaskListV2 to improve column rendering and added state management for field visibility synchronization with the database. - Enhanced AddTaskRow and AddSubtaskRow components for improved user interaction and layout. - Updated placeholder texts in CustomColumnComponents for better clarity.
This commit is contained in:
@@ -26,7 +26,6 @@ const SubtaskLoadingSkeleton: React.FC<SubtaskLoadingSkeletonProps> = ({ visible
|
|||||||
case 'title':
|
case 'title':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle} className="flex items-center">
|
<div style={baseStyle} className="flex items-center">
|
||||||
{/* Subtask indentation - tighter spacing */}
|
|
||||||
<div className="w-4" />
|
<div className="w-4" />
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
<div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
||||||
@@ -35,7 +34,7 @@ const SubtaskLoadingSkeleton: React.FC<SubtaskLoadingSkeletonProps> = ({ visible
|
|||||||
case 'description':
|
case 'description':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle} className="flex items-center px-2">
|
<div style={baseStyle} className="flex items-center px-2">
|
||||||
<div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'status':
|
case 'status':
|
||||||
@@ -66,7 +65,7 @@ const SubtaskLoadingSkeleton: React.FC<SubtaskLoadingSkeletonProps> = ({ visible
|
|||||||
case 'progress':
|
case 'progress':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle} className="flex items-center">
|
<div style={baseStyle} className="flex items-center">
|
||||||
<div className="h-2 w-16 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
|
<div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'labels':
|
case 'labels':
|
||||||
@@ -91,7 +90,7 @@ const SubtaskLoadingSkeleton: React.FC<SubtaskLoadingSkeletonProps> = ({ visible
|
|||||||
case 'estimation':
|
case 'estimation':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle} className="flex items-center">
|
<div style={baseStyle} className="flex items-center">
|
||||||
<div className="h-4 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
<div className="h-4 w-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'startDate':
|
case 'startDate':
|
||||||
@@ -132,7 +131,7 @@ const SubtaskLoadingSkeleton: React.FC<SubtaskLoadingSkeletonProps> = ({ visible
|
|||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
|
<div className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
|
||||||
<div className="flex items-center min-w-max px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center min-w-max px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
{visibleColumns.map((column) => (
|
{visibleColumns.map((column, index) => (
|
||||||
<div key={column.id}>
|
<div key={column.id}>
|
||||||
{renderColumn(column.id, column.width)}
|
{renderColumn(column.id, column.width)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -166,8 +166,6 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
setCategoryModalVisible(true);
|
setCategoryModalVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Handle category change
|
// Handle category change
|
||||||
const handleCategoryChange = useCallback(async (categoryId: string, e?: React.MouseEvent) => {
|
const handleCategoryChange = useCallback(async (categoryId: string, e?: React.MouseEvent) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
@@ -292,107 +290,17 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
<div className="flex items-center flex-1 ml-1">
|
<div className="flex items-center flex-1 ml-1">
|
||||||
{/* Group name and count */}
|
{/* Group name and count */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isEditingName && isOwnerOrAdmin ? (
|
<span
|
||||||
<Input
|
className="text-sm font-semibold"
|
||||||
value={editingName}
|
style={{ color: headerTextColor }}
|
||||||
onChange={(e) => setEditingName(e.target.value)}
|
>
|
||||||
onKeyDown={handleNameKeyDown}
|
{group.name}
|
||||||
onBlur={handleNameBlur}
|
</span>
|
||||||
className="text-sm font-semibold px-2 py-1 rounded-md transition-all duration-200 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
|
||||||
style={{
|
|
||||||
color: headerTextColor,
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: 600,
|
|
||||||
width: `${Math.max(editingName.length * 8 + 16, 80)}px`,
|
|
||||||
minWidth: '80px',
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
border: `1px solid ${headerTextColor}40`,
|
|
||||||
backdropFilter: 'blur(4px)'
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
color: headerTextColor,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
outline: 'none',
|
|
||||||
boxShadow: 'none',
|
|
||||||
padding: '0'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
disabled={isRenaming}
|
|
||||||
placeholder={t('enterGroupName')}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold ${isOwnerOrAdmin ? 'cursor-pointer hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10 rounded px-2 py-1 transition-all duration-200 hover:shadow-sm' : ''}`}
|
|
||||||
onClick={handleNameClick}
|
|
||||||
style={{ color: headerTextColor }}
|
|
||||||
title={isOwnerOrAdmin ? t('clickToEditGroupName') : ''}
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-semibold ml-1" style={{ color: headerTextColor }}>
|
<span className="text-sm font-semibold ml-1" style={{ color: headerTextColor }}>
|
||||||
({group.count})
|
({group.count})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Three dots menu */}
|
|
||||||
<div className="flex items-center justify-center ml-2">
|
|
||||||
<Dropdown
|
|
||||||
menu={{ items: menuItems }}
|
|
||||||
trigger={['click']}
|
|
||||||
open={dropdownVisible}
|
|
||||||
onOpenChange={setDropdownVisible}
|
|
||||||
placement="bottomLeft"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="p-1 rounded-sm hover:bg-black hover:bg-opacity-10 transition-all duration-200 ease-out"
|
|
||||||
style={{ color: headerTextColor }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setDropdownVisible(!dropdownVisible);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Change Category Modal */}
|
|
||||||
<Modal
|
|
||||||
title="Change Category"
|
|
||||||
open={categoryModalVisible}
|
|
||||||
onCancel={() => setCategoryModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={400}
|
|
||||||
>
|
|
||||||
<div className="py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{statusCategories?.map((category) => (
|
|
||||||
<div
|
|
||||||
key={category.id}
|
|
||||||
className="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
onClick={(e) => category.id && handleCategoryChange(category.id, e)}
|
|
||||||
>
|
|
||||||
<Flex align="center" gap={12}>
|
|
||||||
<Badge color={category.color_code} />
|
|
||||||
<span className="font-medium">{category.name}</span>
|
|
||||||
</Flex>
|
|
||||||
{isChangingCategory && (
|
|
||||||
<div className="text-blue-500">
|
|
||||||
<ArrowPathIcon className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
import React, { useCallback, useMemo, useEffect, useState } from 'react';
|
||||||
import { GroupedVirtuoso } from 'react-virtuoso';
|
import { GroupedVirtuoso } from 'react-virtuoso';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -156,7 +156,7 @@ const TaskListV2: React.FC = () => {
|
|||||||
const fieldType = column.custom_column_obj?.fieldType;
|
const fieldType = column.custom_column_obj?.fieldType;
|
||||||
let defaultWidth = 160;
|
let defaultWidth = 160;
|
||||||
if (fieldType === 'selection') {
|
if (fieldType === 'selection') {
|
||||||
defaultWidth = 180; // Extra width for selection dropdowns
|
defaultWidth = 150; // Reduced width for selection dropdowns
|
||||||
} else if (fieldType === 'people') {
|
} else if (fieldType === 'people') {
|
||||||
defaultWidth = 170; // Extra width for people with avatars
|
defaultWidth = 170; // Extra width for people with avatars
|
||||||
}
|
}
|
||||||
@@ -177,36 +177,6 @@ const TaskListV2: React.FC = () => {
|
|||||||
return [...baseVisibleColumns, ...visibleCustomColumns];
|
return [...baseVisibleColumns, ...visibleCustomColumns];
|
||||||
}, [fields, columns, customColumns, t]);
|
}, [fields, columns, customColumns, t]);
|
||||||
|
|
||||||
// Sync local field changes with backend column configuration (debounced)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!urlProjectId || columns.length === 0 || fields.length === 0) return;
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
const changedFields = fields.filter(field => {
|
|
||||||
const backendColumn = columns.find(c => c.key === field.key);
|
|
||||||
if (backendColumn) {
|
|
||||||
return (backendColumn.pinned ?? false) !== field.visible;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
changedFields.forEach(field => {
|
|
||||||
const backendColumn = columns.find(c => c.key === field.key);
|
|
||||||
if (backendColumn) {
|
|
||||||
dispatch(updateColumnVisibility({
|
|
||||||
projectId: urlProjectId,
|
|
||||||
item: {
|
|
||||||
...backendColumn,
|
|
||||||
pinned: field.visible
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [fields, columns, urlProjectId, dispatch]);
|
|
||||||
|
|
||||||
// Effects
|
// Effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlProjectId) {
|
if (urlProjectId) {
|
||||||
@@ -215,6 +185,38 @@ const TaskListV2: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, urlProjectId]);
|
}, [dispatch, urlProjectId]);
|
||||||
|
|
||||||
|
// Initialize field visibility from database when columns are loaded (only once)
|
||||||
|
const [initializedFromDatabase, setInitializedFromDatabase] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (columns.length > 0 && fields.length > 0 && !initializedFromDatabase) {
|
||||||
|
// Update local fields to match database state only on initial load
|
||||||
|
import('@/features/task-management/taskListFields.slice').then(({ setFields }) => {
|
||||||
|
// Create updated fields based on database column state
|
||||||
|
const updatedFields = fields.map(field => {
|
||||||
|
const backendColumn = columns.find(c => c.key === field.key);
|
||||||
|
if (backendColumn) {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
visible: backendColumn.pinned ?? field.visible
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only update if there are actual changes
|
||||||
|
const hasChanges = updatedFields.some((field, index) =>
|
||||||
|
field.visible !== fields[index].visible
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
dispatch(setFields(updatedFields));
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitializedFromDatabase(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [columns, fields, dispatch, initializedFromDatabase]);
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
const handleTaskSelect = useCallback(
|
const handleTaskSelect = useCallback(
|
||||||
(taskId: string, event: React.MouseEvent) => {
|
(taskId: string, event: React.MouseEvent) => {
|
||||||
@@ -360,7 +362,7 @@ const TaskListV2: React.FC = () => {
|
|||||||
{isGroupEmpty && !isGroupCollapsed && (
|
{isGroupEmpty && !isGroupCollapsed && (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="flex items-center min-w-max px-1 py-3">
|
<div className="flex items-center min-w-max px-1 py-3">
|
||||||
{visibleColumns.map((column) => (
|
{visibleColumns.map((column, index) => (
|
||||||
<div
|
<div
|
||||||
key={`empty-${column.id}`}
|
key={`empty-${column.id}`}
|
||||||
style={{ width: column.width, flexShrink: 0 }}
|
style={{ width: column.width, flexShrink: 0 }}
|
||||||
@@ -412,48 +414,48 @@ const TaskListV2: React.FC = () => {
|
|||||||
|
|
||||||
// Render column headers
|
// Render column headers
|
||||||
const renderColumnHeaders = useCallback(() => (
|
const renderColumnHeaders = useCallback(() => (
|
||||||
<div className="sticky top-0 z-30 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
<div className="sticky top-0 z-30 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center px-1 py-3 w-full" style={{ minWidth: 'max-content', height: '44px' }}>
|
<div className="flex items-center px-3 py-3 w-full" style={{ minWidth: 'max-content', height: '44px' }}>
|
||||||
{visibleColumns.map(column => {
|
{visibleColumns.map((column, index) => {
|
||||||
const columnStyle: ColumnStyle = {
|
const columnStyle: ColumnStyle = {
|
||||||
width: column.width,
|
width: column.width,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
...(column.id === 'labels' && column.width === 'auto'
|
...(column.id === 'labels' && column.width === 'auto'
|
||||||
? {
|
? {
|
||||||
minWidth: '200px',
|
minWidth: '200px',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
|
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
|
||||||
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
|
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={column.id}
|
key={column.id}
|
||||||
className={`text-sm font-semibold text-gray-600 dark:text-gray-300 ${
|
className={`text-sm font-semibold text-gray-600 dark:text-gray-300 ${
|
||||||
column.id === 'taskKey' ? 'pl-3' : ''
|
column.id === 'taskKey' ? 'pl-3' : ''
|
||||||
}`}
|
}`}
|
||||||
style={columnStyle}
|
style={columnStyle}
|
||||||
>
|
>
|
||||||
{column.id === 'dragHandle' || column.id === 'checkbox' ? (
|
{column.id === 'dragHandle' || column.id === 'checkbox' ? (
|
||||||
<span></span>
|
<span></span>
|
||||||
) : (column as any).isCustom ? (
|
) : (column as any).isCustom ? (
|
||||||
<CustomColumnHeader
|
<CustomColumnHeader
|
||||||
column={column}
|
column={column}
|
||||||
onSettingsClick={handleCustomColumnSettings}
|
onSettingsClick={handleCustomColumnSettings}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
t(column.label || '')
|
t(column.label || '')
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className="flex items-center justify-center" style={{ width: '60px', flexShrink: 0 }}>
|
|
||||||
<AddCustomColumnButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="flex items-center justify-center" style={{ width: '60px', flexShrink: 0 }}>
|
||||||
|
<AddCustomColumnButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
), [visibleColumns, t, handleCustomColumnSettings]);
|
), [visibleColumns, t, handleCustomColumnSettings]);
|
||||||
|
|
||||||
// Loading and error states
|
// Loading and error states
|
||||||
@@ -470,13 +472,16 @@ const TaskListV2: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col bg-white dark:bg-gray-900" style={{ height: '100vh', overflow: 'hidden' }}>
|
<div className="flex flex-col bg-white dark:bg-gray-900" style={{ height: '100vh', overflow: 'hidden' }}>
|
||||||
{/* Task Filters */}
|
{/* Task Filters */}
|
||||||
<div className="flex-none px-4 py-3" style={{ height: '66px', flexShrink: 0 }}>
|
<div className="flex-none px-6 py-4" style={{ height: '74px', flexShrink: 0 }}>
|
||||||
<ImprovedTaskFilters position="list" />
|
<ImprovedTaskFilters position="list" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Spacing between filters and table */}
|
||||||
|
<div className="flex-none h-4" style={{ flexShrink: 0 }}></div>
|
||||||
|
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-auto border border-gray-200 dark:border-gray-700"
|
className="flex-1 overflow-auto border border-gray-200 dark:border-gray-700 mx-6 rounded-lg"
|
||||||
style={{
|
style={{
|
||||||
height: '600px',
|
height: '600px',
|
||||||
maxHeight: '600px'
|
maxHeight: '600px'
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
case 'dragHandle':
|
case 'dragHandle':
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center ${isSubtask ? '' : 'cursor-grab active:cursor-grabbing'}`}
|
className="flex items-center justify-center"
|
||||||
style={baseStyle}
|
style={baseStyle}
|
||||||
{...(isSubtask ? {} : { ...attributes, ...listeners })}
|
{...(isSubtask ? {} : { ...attributes, ...listeners })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors h-full"
|
className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors h-full"
|
||||||
>
|
>
|
||||||
<PlusOutlined className="text-xs" />
|
<PlusOutlined className="text-xs" />
|
||||||
{t('addSubTaskText')}
|
{t('addSubtaskText')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
@@ -121,7 +121,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
className="w-full h-full border-none shadow-none bg-transparent"
|
className="w-full h-full border-none shadow-none bg-transparent"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
minHeight: '42px',
|
minHeight: '32px',
|
||||||
padding: '0',
|
padding: '0',
|
||||||
fontSize: '14px'
|
fontSize: '14px'
|
||||||
}}
|
}}
|
||||||
@@ -137,10 +137,12 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, t]);
|
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center min-w-max px-1 py-2 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[42px]">
|
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
||||||
{visibleColumns.map((column) =>
|
{visibleColumns.map((column, index) => (
|
||||||
renderColumn(column.id, column.width)
|
<React.Fragment key={column.id}>
|
||||||
)}
|
{renderColumn(column.id, column.width)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -135,9 +135,11 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
||||||
{visibleColumns.map((column) =>
|
{visibleColumns.map((column, index) => (
|
||||||
renderColumn(column.id, column.width)
|
<React.Fragment key={column.id}>
|
||||||
)}
|
{renderColumn(column.id, column.width)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export const DateCustomColumnCell: React.FC<{
|
|||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange}
|
||||||
placeholder={dateValue ? "" : "Click to set date"}
|
placeholder={dateValue ? "" : "Set date"}
|
||||||
format="MMM DD, YYYY"
|
format="MMM DD, YYYY"
|
||||||
suffixIcon={null}
|
suffixIcon={null}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -468,7 +468,7 @@ export const SelectionCustomColumnCell: React.FC<{
|
|||||||
: 'border-gray-200 text-gray-600 bg-gray-50'
|
: 'border-gray-200 text-gray-600 bg-gray-50'
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
Select an option
|
Select option
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Options */}
|
{/* Options */}
|
||||||
@@ -569,7 +569,7 @@ export const SelectionCustomColumnCell: React.FC<{
|
|||||||
<>
|
<>
|
||||||
<div className={`w-3 h-3 rounded-full border-2 border-dashed ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`} />
|
<div className={`w-3 h-3 rounded-full border-2 border-dashed ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`} />
|
||||||
<span className={`text-sm ${isDarkMode ? 'text-gray-500' : 'text-gray-400'}`}>
|
<span className={`text-sm ${isDarkMode ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||||
Select option
|
Select
|
||||||
</span>
|
</span>
|
||||||
<svg className={`w-4 h-4 ml-auto transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''} ${isDarkMode ? 'text-gray-500' : 'text-gray-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className={`w-4 h-4 ml-auto transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''} ${isDarkMode ? 'text-gray-500' : 'text-gray-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import { useAppSelector } from '@/hooks/useAppSelector';
|
|||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
||||||
import { toggleField } 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
|
// Import Redux actions
|
||||||
import {
|
import {
|
||||||
@@ -698,8 +699,10 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
isDarkMode,
|
isDarkMode,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('task-list-filters');
|
const { t } = useTranslation('task-list-filters');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
||||||
|
const columns = useSelector(selectColumns);
|
||||||
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
const fields = Array.isArray(fieldsRaw) ? fieldsRaw : [];
|
const fields = Array.isArray(fieldsRaw) ? fieldsRaw : [];
|
||||||
const sortedFields = useMemo(() => [...fields].sort((a, b) => a.order - b.order), [fields]);
|
const sortedFields = useMemo(() => [...fields].sort((a, b) => a.order - b.order), [fields]);
|
||||||
|
|
||||||
@@ -792,7 +795,20 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={field.key}
|
key={field.key}
|
||||||
onClick={() => dispatch(toggleField(field.key))}
|
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`
|
className={`
|
||||||
w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded
|
w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded
|
||||||
transition-colors duration-150 text-left
|
transition-colors duration-150 text-left
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { updateColumnVisibility } from './task-management.slice';
|
||||||
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
|
|
||||||
export interface TaskListField {
|
export interface TaskListField {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -49,6 +51,71 @@ function saveFields(fields: TaskListField[]) {
|
|||||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fields));
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fields));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Async thunk to sync field visibility with database
|
||||||
|
export const syncFieldWithDatabase = createAsyncThunk(
|
||||||
|
'taskManagementFields/syncFieldWithDatabase',
|
||||||
|
async (
|
||||||
|
{ projectId, fieldKey, visible, columns }: {
|
||||||
|
projectId: string;
|
||||||
|
fieldKey: string;
|
||||||
|
visible: boolean;
|
||||||
|
columns: ITaskListColumn[]
|
||||||
|
},
|
||||||
|
{ dispatch }
|
||||||
|
) => {
|
||||||
|
// Find the corresponding backend column
|
||||||
|
const backendColumn = columns.find(c => c.key === fieldKey);
|
||||||
|
if (backendColumn) {
|
||||||
|
// Update the column visibility in the database
|
||||||
|
await dispatch(updateColumnVisibility({
|
||||||
|
projectId,
|
||||||
|
item: {
|
||||||
|
...backendColumn,
|
||||||
|
pinned: visible
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return { fieldKey, visible };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Async thunk to sync all fields with database
|
||||||
|
export const syncAllFieldsWithDatabase = createAsyncThunk(
|
||||||
|
'taskManagementFields/syncAllFieldsWithDatabase',
|
||||||
|
async (
|
||||||
|
{ projectId, fields, columns }: {
|
||||||
|
projectId: string;
|
||||||
|
fields: TaskListField[];
|
||||||
|
columns: ITaskListColumn[]
|
||||||
|
},
|
||||||
|
{ dispatch }
|
||||||
|
) => {
|
||||||
|
// Find fields that need to be synced
|
||||||
|
const fieldsToSync = fields.filter(field => {
|
||||||
|
const backendColumn = columns.find(c => c.key === field.key);
|
||||||
|
return backendColumn && (backendColumn.pinned ?? false) !== field.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync each field
|
||||||
|
const syncPromises = fieldsToSync.map(field => {
|
||||||
|
const backendColumn = columns.find(c => c.key === field.key);
|
||||||
|
if (backendColumn) {
|
||||||
|
return dispatch(updateColumnVisibility({
|
||||||
|
projectId,
|
||||||
|
item: {
|
||||||
|
...backendColumn,
|
||||||
|
pinned: field.visible
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(syncPromises);
|
||||||
|
return fieldsToSync.map(f => ({ fieldKey: f.key, visible: f.visible }));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const initialState: TaskListField[] = loadFields();
|
const initialState: TaskListField[] = loadFields();
|
||||||
|
|
||||||
const taskListFieldsSlice = createSlice({
|
const taskListFieldsSlice = createSlice({
|
||||||
@@ -75,10 +142,42 @@ const taskListFieldsSlice = createSlice({
|
|||||||
saveFields(defaultFields);
|
saveFields(defaultFields);
|
||||||
return defaultFields;
|
return defaultFields;
|
||||||
},
|
},
|
||||||
|
// New action to update field visibility from database
|
||||||
|
updateFieldVisibilityFromDatabase(state, action: PayloadAction<{ fieldKey: string; visible: boolean }>) {
|
||||||
|
const { fieldKey, visible } = action.payload;
|
||||||
|
const field = state.find(f => f.key === fieldKey);
|
||||||
|
if (field) {
|
||||||
|
field.visible = visible;
|
||||||
|
// Save to localStorage
|
||||||
|
saveFields(state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(syncFieldWithDatabase.fulfilled, (state, action) => {
|
||||||
|
// Field visibility has been synced with database
|
||||||
|
const { fieldKey, visible } = action.payload;
|
||||||
|
const field = state.find(f => f.key === fieldKey);
|
||||||
|
if (field) {
|
||||||
|
field.visible = visible;
|
||||||
|
saveFields(state);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(syncAllFieldsWithDatabase.fulfilled, (state, action) => {
|
||||||
|
// All fields have been synced with database
|
||||||
|
action.payload.forEach(({ fieldKey, visible }) => {
|
||||||
|
const field = state.find(f => f.key === fieldKey);
|
||||||
|
if (field) {
|
||||||
|
field.visible = visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveFields(state);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { toggleField, setFields, resetFields } = taskListFieldsSlice.actions;
|
export const { toggleField, setFields, resetFields, updateFieldVisibilityFromDatabase } = taskListFieldsSlice.actions;
|
||||||
|
|
||||||
// Utility function to force reset fields (can be called from browser console)
|
// Utility function to force reset fields (can be called from browser console)
|
||||||
export const forceResetFields = () => {
|
export const forceResetFields = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user