feat(i18n): enhance internationalization support and optimize task updates
- Introduced I18nReadyCheck component to ensure translations are loaded before rendering the app. - Updated various components to utilize the new i18n structure, improving translation management. - Implemented optimistic UI updates for task properties during drag-and-drop operations, enhancing responsiveness. - Added checks to prevent stale updates from socket events, ensuring the UI reflects the most recent task states. - Enhanced error handling and logging for i18n initialization and loading processes.
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
// Core dependencies
|
// Core dependencies
|
||||||
import React, { Suspense, useEffect, memo, useMemo, useCallback } from 'react';
|
import React, { Suspense, useEffect, memo, useMemo, useCallback, useState } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||||
@@ -27,6 +28,24 @@ import { CSSPerformanceMonitor, LayoutStabilizer, CriticalCSSManager } from './u
|
|||||||
// Service Worker
|
// Service Worker
|
||||||
import { registerSW } from './utils/serviceWorkerRegistration';
|
import { registerSW } from './utils/serviceWorkerRegistration';
|
||||||
|
|
||||||
|
// i18n ready check component
|
||||||
|
const I18nReadyCheck: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { ready } = useTranslation();
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ready) {
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
}, [ready]);
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
return <SuspenseFallback />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main App Component - Performance Optimized
|
* Main App Component - Performance Optimized
|
||||||
*
|
*
|
||||||
@@ -200,18 +219,15 @@ const App: React.FC = memo(() => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<I18nReadyCheck>
|
||||||
<ThemeWrapper>
|
|
||||||
<ModuleErrorBoundary>
|
<ModuleErrorBoundary>
|
||||||
<RouterProvider
|
<ThemeWrapper>
|
||||||
router={router}
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
future={{
|
<RouterProvider router={router} />
|
||||||
v7_startTransition: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModuleErrorBoundary>
|
|
||||||
</ThemeWrapper>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</ThemeWrapper>
|
||||||
|
</ModuleErrorBoundary>
|
||||||
|
</I18nReadyCheck>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,30 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
newPosition: insertIndex,
|
newPosition: insertIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Optimistically update task properties immediately for UI responsiveness
|
||||||
|
const updatedTask = {
|
||||||
|
...activeTask,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update task properties based on the grouping type
|
||||||
|
if (currentGrouping === 'priority') {
|
||||||
|
updatedTask.priority = targetGroup.id;
|
||||||
|
updatedTask.priority_id = targetGroup.id;
|
||||||
|
} else if (currentGrouping === 'phase') {
|
||||||
|
updatedTask.phase = targetGroup.id;
|
||||||
|
updatedTask.phase_id = targetGroup.id;
|
||||||
|
} else if (currentGrouping === 'status') {
|
||||||
|
updatedTask.status = targetGroup.id;
|
||||||
|
updatedTask.status_id = targetGroup.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and dispatch updateTask for immediate UI update
|
||||||
|
import('@/features/task-management/task-management.slice').then(({ updateTask }) => {
|
||||||
|
dispatch(updateTask(updatedTask));
|
||||||
|
});
|
||||||
|
|
||||||
// reorderTasksInGroup handles both same-group and cross-group moves
|
// reorderTasksInGroup handles both same-group and cross-group moves
|
||||||
// No need for separate moveTaskBetweenGroups call
|
// No need for separate moveTaskBetweenGroups call
|
||||||
dispatch(
|
dispatch(
|
||||||
|
|||||||
@@ -657,9 +657,15 @@ const taskManagementSlice = createSlice({
|
|||||||
const group = state.groups.find(g => g.id === sourceGroupId);
|
const group = state.groups.find(g => g.id === sourceGroupId);
|
||||||
if (group) {
|
if (group) {
|
||||||
const newTasks = Array.from(group.taskIds);
|
const newTasks = Array.from(group.taskIds);
|
||||||
const [removed] = newTasks.splice(newTasks.indexOf(sourceTaskId), 1);
|
const sourceIndex = newTasks.indexOf(sourceTaskId);
|
||||||
newTasks.splice(newTasks.indexOf(destinationTaskId), 0, removed);
|
const destIndex = newTasks.indexOf(destinationTaskId);
|
||||||
|
|
||||||
|
// Only proceed if both tasks are found
|
||||||
|
if (sourceIndex !== -1 && destIndex !== -1) {
|
||||||
|
const [removed] = newTasks.splice(sourceIndex, 1);
|
||||||
|
newTasks.splice(destIndex, 0, removed);
|
||||||
group.taskIds = newTasks;
|
group.taskIds = newTasks;
|
||||||
|
}
|
||||||
|
|
||||||
// Update order for affected tasks using the appropriate sort field
|
// Update order for affected tasks using the appropriate sort field
|
||||||
const sortField = getSortOrderField(state.grouping?.id);
|
const sortField = getSortOrderField(state.grouping?.id);
|
||||||
@@ -681,13 +687,20 @@ const taskManagementSlice = createSlice({
|
|||||||
// Add to destination group at the correct position relative to destinationTask
|
// Add to destination group at the correct position relative to destinationTask
|
||||||
const destinationIndex = destinationGroup.taskIds.indexOf(destinationTaskId);
|
const destinationIndex = destinationGroup.taskIds.indexOf(destinationTaskId);
|
||||||
if (destinationIndex !== -1) {
|
if (destinationIndex !== -1) {
|
||||||
|
// Ensure we don't add duplicate task IDs
|
||||||
|
if (!destinationGroup.taskIds.includes(sourceTaskId)) {
|
||||||
destinationGroup.taskIds.splice(destinationIndex, 0, sourceTaskId);
|
destinationGroup.taskIds.splice(destinationIndex, 0, sourceTaskId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
destinationGroup.taskIds.push(sourceTaskId); // Add to end if destination task not found
|
// Add to end if destination task not found, but only if not already present
|
||||||
|
if (!destinationGroup.taskIds.includes(sourceTaskId)) {
|
||||||
|
destinationGroup.taskIds.push(sourceTaskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do NOT update the task's grouping field (priority, phase, status) here.
|
// Note: Task's grouping field (priority, phase, status) will also be updated
|
||||||
// This will be handled by the socket event handler after backend confirmation.
|
// by the socket event handler after backend confirmation, but for immediate UI
|
||||||
|
// feedback, we update it optimistically in the drag and drop handler.
|
||||||
|
|
||||||
// Update order for affected tasks in both groups using the appropriate sort field
|
// Update order for affected tasks in both groups using the appropriate sort field
|
||||||
const sortField = getSortOrderField(state.grouping?.id);
|
const sortField = getSortOrderField(state.grouping?.id);
|
||||||
|
|||||||
@@ -217,6 +217,17 @@ export const useTaskSocketHandlers = () => {
|
|||||||
const currentGrouping = state.taskManagement.grouping;
|
const currentGrouping = state.taskManagement.grouping;
|
||||||
|
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
|
// Check if current task has a more recent optimistic update
|
||||||
|
const currentUpdateTime = currentTask.updatedAt || currentTask.updated_at;
|
||||||
|
const serverUpdateTime = response.updated_at || response.updatedAt;
|
||||||
|
|
||||||
|
// If current task was updated more recently than server data, skip this update
|
||||||
|
if (currentUpdateTime && serverUpdateTime &&
|
||||||
|
new Date(currentUpdateTime).getTime() > new Date(serverUpdateTime).getTime()) {
|
||||||
|
console.log(`[TASK_STATUS_CHANGE] Skipping stale update for task ${response.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the new status value based on status category
|
// Determine the new status value based on status category
|
||||||
let newStatusValue: 'todo' | 'doing' | 'done' = 'todo';
|
let newStatusValue: 'todo' | 'doing' | 'done' = 'todo';
|
||||||
if (response.statusCategory) {
|
if (response.statusCategory) {
|
||||||
@@ -393,6 +404,17 @@ export const useTaskSocketHandlers = () => {
|
|||||||
const currentGrouping = state.taskManagement.grouping;
|
const currentGrouping = state.taskManagement.grouping;
|
||||||
|
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
|
// Check if current task has a more recent optimistic update
|
||||||
|
const currentUpdateTime = currentTask.updatedAt || currentTask.updated_at;
|
||||||
|
const serverUpdateTime = response.updated_at || response.updatedAt;
|
||||||
|
|
||||||
|
// If current task was updated more recently than server data, skip this update
|
||||||
|
if (currentUpdateTime && serverUpdateTime &&
|
||||||
|
new Date(currentUpdateTime).getTime() > new Date(serverUpdateTime).getTime()) {
|
||||||
|
console.log(`[TASK_PRIORITY_CHANGE] Skipping stale update for task ${response.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get priority list to map priority_id to priority name
|
// Get priority list to map priority_id to priority name
|
||||||
const priorityList = state.priorityReducer?.priorities || [];
|
const priorityList = state.priorityReducer?.priorities || [];
|
||||||
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
|
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
|
||||||
@@ -549,6 +571,17 @@ export const useTaskSocketHandlers = () => {
|
|||||||
const currentTask = state.taskManagement.entities[taskId];
|
const currentTask = state.taskManagement.entities[taskId];
|
||||||
|
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
|
// Check if current task has a more recent optimistic update
|
||||||
|
const currentUpdateTime = currentTask.updatedAt || currentTask.updated_at;
|
||||||
|
const serverUpdateTime = data.updated_at || data.updatedAt;
|
||||||
|
|
||||||
|
// If current task was updated more recently than server data, skip this update
|
||||||
|
if (currentUpdateTime && serverUpdateTime &&
|
||||||
|
new Date(currentUpdateTime).getTime() > new Date(serverUpdateTime).getTime()) {
|
||||||
|
console.log(`[TASK_PHASE_CHANGE] Skipping stale update for task ${data.task_id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get phase list to map phase_id to phase name
|
// Get phase list to map phase_id to phase name
|
||||||
const phaseList = state.phaseReducer?.phaseList || [];
|
const phaseList = state.phaseReducer?.phaseList || [];
|
||||||
let newPhaseValue = '';
|
let newPhaseValue = '';
|
||||||
@@ -1007,6 +1040,18 @@ export const useTaskSocketHandlers = () => {
|
|||||||
data.forEach((taskData: any) => {
|
data.forEach((taskData: any) => {
|
||||||
const currentTask = state.taskManagement.entities[taskData.id];
|
const currentTask = state.taskManagement.entities[taskData.id];
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
|
// Check if current task has a more recent optimistic update
|
||||||
|
const currentUpdateTime = currentTask.updatedAt || currentTask.updated_at;
|
||||||
|
const serverUpdateTime = taskData.updated_at || taskData.updatedAt;
|
||||||
|
|
||||||
|
// If current task was updated more recently than server data, skip this update
|
||||||
|
// This prevents socket updates from overwriting newer optimistic updates
|
||||||
|
if (currentUpdateTime && serverUpdateTime &&
|
||||||
|
new Date(currentUpdateTime).getTime() > new Date(serverUpdateTime).getTime()) {
|
||||||
|
console.log(`[TASK_SORT_ORDER_CHANGE] Skipping stale update for task ${taskData.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let updatedTask: Task = {
|
let updatedTask: Task = {
|
||||||
...currentTask,
|
...currentTask,
|
||||||
order: taskData.sort_order || taskData.current_sort_order || currentTask.order,
|
order: taskData.sort_order || taskData.current_sort_order || currentTask.order,
|
||||||
|
|||||||
@@ -24,11 +24,33 @@ i18n
|
|||||||
|
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
|
// Ensure translations are loaded synchronously
|
||||||
|
crossDomain: false,
|
||||||
|
withCredentials: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
react: {
|
react: {
|
||||||
useSuspense: false,
|
useSuspense: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Ensure all namespaces are loaded upfront
|
||||||
|
ns: ['common', 'home', 'task-management', 'task-list-table'],
|
||||||
|
|
||||||
|
// Add initialization promise to ensure translations are loaded
|
||||||
|
initImmediate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure translations are loaded before the app starts
|
||||||
|
i18n.on('initialized', () => {
|
||||||
|
console.log('i18n initialized successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
i18n.on('loaded', (loaded) => {
|
||||||
|
console.log('i18n loaded:', loaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
i18n.on('failedLoading', (lng, ns, msg) => {
|
||||||
|
console.error('i18n failed loading:', lng, ns, msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { IMyTask } from '@/types/home/my-tasks.types';
|
|||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
|
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Skeleton } from 'antd';
|
||||||
|
|
||||||
interface AddTaskInlineFormProps {
|
interface AddTaskInlineFormProps {
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
@@ -25,35 +27,41 @@ const AddTaskInlineForm = ({ t, calendarView }: AddTaskInlineFormProps) => {
|
|||||||
const [isDueDateFieldShowing, setIsDueDateFieldShowing] = useState(false);
|
const [isDueDateFieldShowing, setIsDueDateFieldShowing] = useState(false);
|
||||||
const [isProjectFieldShowing, setIsProjectFieldShowing] = useState(false);
|
const [isProjectFieldShowing, setIsProjectFieldShowing] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { ready } = useTranslation('home');
|
||||||
|
|
||||||
|
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
|
||||||
|
const { data: projectListData, isFetching: projectListFetching } = useGetProjectsByTeamQuery();
|
||||||
|
const { refetch } = useGetMyTasksQuery(homeTasksConfig);
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const { data: projectListData, isFetching: projectListFetching } = useGetProjectsByTeamQuery();
|
|
||||||
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
|
|
||||||
const { refetch } = useGetMyTasksQuery(homeTasksConfig);
|
|
||||||
|
|
||||||
const taskInputRef = useRef<InputRef | null>(null);
|
const taskInputRef = useRef<InputRef | null>(null);
|
||||||
|
|
||||||
|
// Don't render until i18n is ready
|
||||||
|
if (!ready) {
|
||||||
|
return <Skeleton active />;
|
||||||
|
}
|
||||||
|
|
||||||
const dueDateOptions = [
|
const dueDateOptions = [
|
||||||
{
|
{
|
||||||
value: 'Today',
|
value: 'Today',
|
||||||
label: t('home:tasks.today'),
|
label: t('tasks.today'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Tomorrow',
|
value: 'Tomorrow',
|
||||||
label: t('home:tasks.tomorrow'),
|
label: t('tasks.tomorrow'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Next Week',
|
value: 'Next Week',
|
||||||
label: t('home:tasks.nextWeek'),
|
label: t('tasks.nextWeek'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Next Month',
|
value: 'Next Month',
|
||||||
label: t('home:tasks.nextMonth'),
|
label: t('tasks.nextMonth'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'No Due Date',
|
value: 'No Due Date',
|
||||||
label: t('home:tasks.noDueDate'),
|
label: t('tasks.noDueDate'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -169,14 +177,14 @@ const AddTaskInlineForm = ({ t, calendarView }: AddTaskInlineFormProps) => {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t('home:tasks.taskRequired'),
|
message: t('tasks.taskRequired'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Flex vertical gap={4}>
|
<Flex vertical gap={4}>
|
||||||
<Input
|
<Input
|
||||||
ref={taskInputRef}
|
ref={taskInputRef}
|
||||||
placeholder={t('home:tasks.addTask')}
|
placeholder={t('tasks.addTask')}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const inputValue = e.currentTarget.value;
|
const inputValue = e.currentTarget.value;
|
||||||
@@ -200,7 +208,7 @@ const AddTaskInlineForm = ({ t, calendarView }: AddTaskInlineFormProps) => {
|
|||||||
<Alert
|
<Alert
|
||||||
message={
|
message={
|
||||||
<Typography.Text style={{ fontSize: 11 }}>
|
<Typography.Text style={{ fontSize: 11 }}>
|
||||||
{t('home:tasks.pressTabToSelectDueDateAndProject')}
|
{t('tasks.pressTabToSelectDueDateAndProject')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
}
|
}
|
||||||
type="info"
|
type="info"
|
||||||
@@ -245,7 +253,7 @@ const AddTaskInlineForm = ({ t, calendarView }: AddTaskInlineFormProps) => {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t('home:tasks.projectRequired'),
|
message: t('tasks.projectRequired'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import HomeCalendar from '../../../components/calendars/homeCalendar/HomeCalendar';
|
import HomeCalendar from '../../../components/calendars/homeCalendar/HomeCalendar';
|
||||||
import { Tag, Typography } from 'antd';
|
import { Tag, Typography, Skeleton } from 'antd';
|
||||||
import { ClockCircleOutlined } from '@ant-design/icons';
|
import { ClockCircleOutlined } from '@ant-design/icons';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import AddTaskInlineForm from './add-task-inline-form';
|
import AddTaskInlineForm from './add-task-inline-form';
|
||||||
@@ -10,7 +10,12 @@ import dayjs from 'dayjs';
|
|||||||
|
|
||||||
const CalendarView = () => {
|
const CalendarView = () => {
|
||||||
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
|
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
|
||||||
const { t } = useTranslation('home');
|
const { t, ready } = useTranslation('home');
|
||||||
|
|
||||||
|
// Don't render until i18n is ready
|
||||||
|
if (!ready) {
|
||||||
|
return <Skeleton active />;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!homeTasksConfig.selected_date) {
|
if (!homeTasksConfig.selected_date) {
|
||||||
@@ -36,7 +41,7 @@ const CalendarView = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{t('home:tasks.dueOn')} {homeTasksConfig.selected_date?.format('MMM DD, YYYY')}
|
{t('tasks.dueOn')} {homeTasksConfig.selected_date?.format('MMM DD, YYYY')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
||||||
|
|||||||
@@ -61,18 +61,23 @@ const TasksList: React.FC = React.memo(() => {
|
|||||||
refetchOnFocus: false,
|
refetchOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation('home');
|
const { t, ready } = useTranslation('home');
|
||||||
const { model } = useAppSelector(state => state.homePageReducer);
|
const { model } = useAppSelector(state => state.homePageReducer);
|
||||||
|
|
||||||
|
// Don't render until i18n is ready
|
||||||
|
if (!ready) {
|
||||||
|
return <Skeleton active />;
|
||||||
|
}
|
||||||
|
|
||||||
const taskModes = useMemo(
|
const taskModes = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
value: 0,
|
value: 0,
|
||||||
label: t('home:tasks.assignedToMe'),
|
label: t('tasks.assignedToMe'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 1,
|
value: 1,
|
||||||
label: t('home:tasks.assignedByMe'),
|
label: t('tasks.assignedByMe'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Tooltip from 'antd/es/tooltip';
|
|||||||
import Typography from 'antd/es/typography';
|
import Typography from 'antd/es/typography';
|
||||||
import Button from 'antd/es/button';
|
import Button from 'antd/es/button';
|
||||||
import Alert from 'antd/es/alert';
|
import Alert from 'antd/es/alert';
|
||||||
|
import Skeleton from 'antd/es/skeleton';
|
||||||
|
|
||||||
import EmptyListPlaceholder from '@components/EmptyListPlaceholder';
|
import EmptyListPlaceholder from '@components/EmptyListPlaceholder';
|
||||||
import { IMyTask } from '@/types/home/my-tasks.types';
|
import { IMyTask } from '@/types/home/my-tasks.types';
|
||||||
@@ -24,7 +25,7 @@ import { useCreatePersonalTaskMutation } from '@/api/home-page/home-page.api.ser
|
|||||||
const TodoList = () => {
|
const TodoList = () => {
|
||||||
const [isAlertShowing, setIsAlertShowing] = useState(false);
|
const [isAlertShowing, setIsAlertShowing] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation('home');
|
const { t, ready } = useTranslation('home');
|
||||||
|
|
||||||
const [createPersonalTask, { isLoading: isCreatingPersonalTask }] =
|
const [createPersonalTask, { isLoading: isCreatingPersonalTask }] =
|
||||||
useCreatePersonalTaskMutation();
|
useCreatePersonalTaskMutation();
|
||||||
@@ -35,6 +36,11 @@ const TodoList = () => {
|
|||||||
// ref for todo input field
|
// ref for todo input field
|
||||||
const todoInputRef = useRef<InputRef | null>(null);
|
const todoInputRef = useRef<InputRef | null>(null);
|
||||||
|
|
||||||
|
// Don't render until i18n is ready
|
||||||
|
if (!ready) {
|
||||||
|
return <Skeleton active />;
|
||||||
|
}
|
||||||
|
|
||||||
// function to handle todo submit
|
// function to handle todo submit
|
||||||
const handleTodoSubmit = async (values: any) => {
|
const handleTodoSubmit = async (values: any) => {
|
||||||
if (!values.name || values.name.trim() === '') return;
|
if (!values.name || values.name.trim() === '') return;
|
||||||
@@ -43,6 +49,7 @@ const TodoList = () => {
|
|||||||
done: false,
|
done: false,
|
||||||
is_task: false,
|
is_task: false,
|
||||||
color_code: '#000',
|
color_code: '#000',
|
||||||
|
manual_progress: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await createPersonalTask(newTodo);
|
const res = await createPersonalTask(newTodo);
|
||||||
@@ -69,7 +76,7 @@ const TodoList = () => {
|
|||||||
width: 32,
|
width: 32,
|
||||||
render: (record: IMyTask) => (
|
render: (record: IMyTask) => (
|
||||||
<ConfigProvider wave={{ disabled: true }}>
|
<ConfigProvider wave={{ disabled: true }}>
|
||||||
<Tooltip title={t('home:todoList.markAsDone')}>
|
<Tooltip title={t('todoList.markAsDone')}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
className="borderless-icon-btn"
|
className="borderless-icon-btn"
|
||||||
@@ -100,11 +107,11 @@ const TodoList = () => {
|
|||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<Typography.Title level={5} style={{ marginBlockEnd: 0 }}>
|
<Typography.Title level={5} style={{ marginBlockEnd: 0 }}>
|
||||||
{t('home:todoList.title')} ({data?.body.length})
|
{t('todoList.title')} ({data?.body.length})
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
<Tooltip title={t('home:todoList.refreshTasks')}>
|
<Tooltip title={t('todoList.refreshTasks')}>
|
||||||
<Button shape="circle" icon={<SyncOutlined spin={isFetching} />} onClick={refetch} />
|
<Button shape="circle" icon={<SyncOutlined spin={isFetching} />} onClick={refetch} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
@@ -116,7 +123,7 @@ const TodoList = () => {
|
|||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<Input
|
<Input
|
||||||
ref={todoInputRef}
|
ref={todoInputRef}
|
||||||
placeholder={t('home:todoList.addTask')}
|
placeholder={t('todoList.addTask')}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const inputValue = e.currentTarget.value;
|
const inputValue = e.currentTarget.value;
|
||||||
|
|
||||||
@@ -128,8 +135,8 @@ const TodoList = () => {
|
|||||||
<Alert
|
<Alert
|
||||||
message={
|
message={
|
||||||
<Typography.Text style={{ fontSize: 11 }}>
|
<Typography.Text style={{ fontSize: 11 }}>
|
||||||
{t('home:todoList.pressEnter')} <strong>Enter</strong>{' '}
|
{t('todoList.pressEnter')} <strong>Enter</strong>{' '}
|
||||||
{t('home:todoList.toCreate')}
|
{t('todoList.toCreate')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
}
|
}
|
||||||
type="info"
|
type="info"
|
||||||
@@ -148,7 +155,7 @@ const TodoList = () => {
|
|||||||
{data?.body.length === 0 ? (
|
{data?.body.length === 0 ? (
|
||||||
<EmptyListPlaceholder
|
<EmptyListPlaceholder
|
||||||
imageSrc="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp"
|
imageSrc="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp"
|
||||||
text={t('home:todoList.noTasks')}
|
text={t('todoList.noTasks')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
Reference in New Issue
Block a user