From 1fff9b691f794843fb16566c30855904b95fbdba Mon Sep 17 00:00:00 2001 From: Sergey Yaskov Date: Sun, 17 Nov 2024 13:06:16 +0200 Subject: [PATCH] If you load the calendar, then go to inbox, then set a date to a task, then switch back to the calendar, then the calendar won't be updated #76 - Store all the lists including the schedule in the state.tasks.taskLists. - Send a request to the server even when you change the order of the tasks inside the same schedule section. This will be needed to be able to store the order of the tasks inside a schedule section. - Use tags to fix the original issue. --- src/components/Inbox/InboxView.tsx | 4 +-- src/components/Root.tsx | 3 +- src/components/Schedule/ScheduleView.tsx | 46 ++++++++++-------------- src/models/appModel.ts | 9 +---- src/redux/api.ts | 17 +++++---- src/redux/reducers/scheduleSlice.ts | 46 ------------------------ src/redux/reducers/tasksSlice.ts | 22 ++++++++++-- src/redux/store.ts | 2 -- src/redux/tags.ts | 1 + src/utils/dragAndDropUtils.ts | 3 -- 10 files changed, 54 insertions(+), 99 deletions(-) delete mode 100644 src/redux/reducers/scheduleSlice.ts create mode 100644 src/redux/tags.ts diff --git a/src/components/Inbox/InboxView.tsx b/src/components/Inbox/InboxView.tsx index 8374118..5d5d6a7 100644 --- a/src/components/Inbox/InboxView.tsx +++ b/src/components/Inbox/InboxView.tsx @@ -47,11 +47,11 @@ const InboxView: FC = () => { }, [updateTaskMutation]) const updateTaskPositionIndex: OnDragEndResponder = useCallback((result) => { - const { source, destination } = result + const { draggableId, source, destination } = result if (userReallyChangedOrder(source, destination)) { updateTasksOrderAsyncMutation({ sourceListType: source.droppableId, - sourceIndex: source.index, + taskId: parseInt(draggableId), destinationListType: source.droppableId, destinationIndex: destination!.index }) diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 82bb1ab..bb3154c 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -10,6 +10,7 @@ import Spinner from './common/Spinner' import ProtectedRoute from './ProtectedRoute' import { TASK_LIST_ID } from '../models/appModel' import { DEFAULT_LIMIT } from '../config' +import { SCHEDULE_TAG } from '../redux/tags' const Root: FC = () => { const dispatch = useDispatch() @@ -24,7 +25,7 @@ const Root: FC = () => { const handleSynchronize = useCallback(() => { dispatch(resetTaskLists()) - dispatch(api.util.invalidateTags(['Schedule'])); + dispatch(api.util.invalidateTags([SCHEDULE_TAG])); [TASK_LIST_ID.INBOX, TASK_LIST_ID.CLOSED].forEach(taskListId => fetchTaskLists({ type: taskListId, offset: 0, limit: DEFAULT_LIMIT })) }, []) diff --git a/src/components/Schedule/ScheduleView.tsx b/src/components/Schedule/ScheduleView.tsx index 8cdf032..6f9f265 100644 --- a/src/components/Schedule/ScheduleView.tsx +++ b/src/components/Schedule/ScheduleView.tsx @@ -1,47 +1,39 @@ -import { DateTime } from 'luxon' import React, { FC, useCallback } from 'react' import { DragDropContext, DraggableLocation, DropResult } from '@hello-pangea/dnd' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { updateScheduleTaskPositionIndex } from '../../redux/reducers/scheduleSlice' import { RootState } from '../../redux/store' -import { useCloseTaskMutation, useFetchScheduleQuery, useUpdateTaskMutation } from '../../redux/api' -import { IScheduleTaskPositionIndex, UpdateTaskRequest } from '../../models/appModel' +import { useCloseTaskMutation, useFetchScheduleQuery, useUpdateTaskMutation, useUpdateTasksOrderAsyncMutation } from '../../redux/api' +import { UpdateTaskRequest } from '../../models/appModel' import SpinnerView from '../common/Spinner' -import { userChangedLists, userReallyChangedOrder } from '../../utils/dragAndDropUtils' +import { userReallyChangedOrder } from '../../utils/dragAndDropUtils' import DroppableTaskListWithHeader from './DroppableTaskListWithHeader' const ScheduleView: FC = () => { const { t } = useTranslation() const dispatch = useDispatch() - const schedule = useSelector((state: RootState) => state.schedule) + const byId = useSelector((state: RootState) => state.tasks.byId) + const schedule = useSelector((state: RootState) => state.tasks.taskLists) const { isLoading, isFetching } = useFetchScheduleQuery() const [updateTaskMutation] = useUpdateTaskMutation() + const [updateTasksOrderAsyncMutation] = useUpdateTasksOrderAsyncMutation() const [closeTaskMutation] = useCloseTaskMutation() const updateTaskPositionIndex = useCallback((result: DropResult) => { - const { source, destination } = result + const { draggableId, source, destination } = result if (userReallyChangedOrder(source, destination as DraggableLocation)) { - const scheduleTaskPositionIndex: IScheduleTaskPositionIndex = { - sourceDroppableId: source.droppableId, - sourceIndex: source.index, - destinationDroppableId: destination!.droppableId, - destinationIndex: destination!.index - } - const newDay = destination!.droppableId - if (userChangedLists(source, destination as DraggableLocation) && newDay !== 'overdue') { - const taskId = parseInt(result.draggableId) - const dueDate = newDay === 'future' - ? DateTime.utc().plus({ weeks: 1 }).endOf('day').toUTC() - : DateTime.fromISO(newDay!).toUTC() - updateTaskMutation({ id: taskId, request: { dueDate } }) + if (newDay !== 'overdue') { + updateTasksOrderAsyncMutation({ + sourceListType: source.droppableId, + taskId: parseInt(draggableId), + destinationListType: destination!.droppableId, + destinationIndex: destination!.index + }) } - - dispatch(updateScheduleTaskPositionIndex(scheduleTaskPositionIndex)) } - }, [dispatch, updateTaskMutation]) + }, [dispatch, updateTasksOrderAsyncMutation]) const closeTask = useCallback(async (id: number) => { await closeTaskMutation(id) @@ -55,7 +47,7 @@ const ScheduleView: FC = () => { const weekdays = Object .keys(schedule) - .filter(day => !['future', 'overdue'].includes(day)) + .filter(day => !['INBOX', 'CLOSED', 'future', 'overdue'].includes(day)) return ( @@ -66,7 +58,7 @@ const ScheduleView: FC = () => { droppableId={date} isDraggable header={t('dueDate', { date })} - tasks={schedule[date]} + tasks={schedule[date].allIds.map(id => byId[id])} onTaskClose={closeTask} onSaveTask={updateTask} />) @@ -75,7 +67,7 @@ const ScheduleView: FC = () => { droppableId="future" isDraggable header={t('futureTasks')} - tasks={schedule.future} + tasks={schedule['future'].allIds.map(id => byId[id])} onTaskClose={closeTask} onSaveTask={updateTask} /> @@ -84,7 +76,7 @@ const ScheduleView: FC = () => { isDraggable isDropDisabled header={t('overdueTasks')} - tasks={schedule.overdue} + tasks={schedule['overdue'].allIds.map(id => byId[id])} onTaskClose={closeTask} onSaveTask={updateTask} /> diff --git a/src/models/appModel.ts b/src/models/appModel.ts index cbe6282..4d0902b 100644 --- a/src/models/appModel.ts +++ b/src/models/appModel.ts @@ -1,12 +1,5 @@ import { DateTime } from 'luxon' -export type IScheduleTaskPositionIndex = { - sourceDroppableId: string - destinationDroppableId: string - sourceIndex: number - destinationIndex: number -} - export type IPage = { size: number number: number @@ -53,8 +46,8 @@ export type DueDateExtractionResult = { export type ITaskPositionIndex = { sourceListType: string + taskId: number destinationListType: string - sourceIndex: number destinationIndex: number } diff --git a/src/redux/api.ts b/src/redux/api.ts index 9ddaf75..77937f1 100644 --- a/src/redux/api.ts +++ b/src/redux/api.ts @@ -12,11 +12,12 @@ import { } from '../models/appModel' import { API_URL } from '../config' import { DateTime } from 'luxon' +import { SCHEDULE_TAG } from './tags' export const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: API_URL, credentials: 'include' }), - tagTypes: ['Schedule'], + tagTypes: [SCHEDULE_TAG], endpoints: (builder) => ({ rephraseTask: builder.mutation({ query: (originalTask) => ({ @@ -35,9 +36,9 @@ export const api = createApi({ future: response.future, overdue: response.overdue }), - providesTags: ['Schedule'] + providesTags: [SCHEDULE_TAG] }), - updateTasksOrderAsync: builder.mutation({ + updateTasksOrderAsync: builder.mutation({ query: (taskPositionIndex) => ({ url: '/orders', method: 'POST', @@ -50,27 +51,29 @@ export const api = createApi({ method: 'POST', body: task }), - invalidatesTags: (result) => result?.dueDate ? ['Schedule'] : [] + invalidatesTags: (result) => result?.dueDate ? [SCHEDULE_TAG] : [] }), closeTask: builder.mutation({ query: (id) => ({ url: `/tasks/${id}/closing`, method: 'PUT' - }) + }), + invalidatesTags: () => [SCHEDULE_TAG] }), reopenTask: builder.mutation({ query: (id) => ({ url: `/tasks/${id}/reopen`, method: 'PUT' }), - invalidatesTags: (result) => result?.dueDate ? ['Schedule'] : [] + invalidatesTags: (result) => result?.dueDate ? [SCHEDULE_TAG] : [] }), updateTask: builder.mutation({ query: ({ id, request }) => ({ url: `/tasks/${id}`, method: 'PUT', body: request - }) + }), + invalidatesTags: () => [SCHEDULE_TAG] }) }) }) diff --git a/src/redux/reducers/scheduleSlice.ts b/src/redux/reducers/scheduleSlice.ts deleted file mode 100644 index 5040e7d..0000000 --- a/src/redux/reducers/scheduleSlice.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { ISchedule, IScheduleTaskPositionIndex } from '../../models/appModel' -import { api } from '../api' - -const INITIAL_STATE: ISchedule = { overdue: [], future: [] } - -const scheduleSlice = createSlice({ - name: 'schedule', - initialState: INITIAL_STATE, - reducers: { - updateScheduleTaskPositionIndex(state, action: PayloadAction) { - const { sourceDroppableId, sourceIndex, destinationDroppableId, destinationIndex } = action.payload - const task = state[sourceDroppableId][sourceIndex] - state[sourceDroppableId].splice(sourceIndex, 1) - state[destinationDroppableId].splice(destinationIndex, 0, task) - } - }, - extraReducers: (builder) => { - builder - .addMatcher( - api.endpoints.updateTask.matchFulfilled, - (state, { payload }) => { - Object.keys(state).forEach(day => { - state[day] = state[day].map(task => task.id === payload.id ? payload : task) - }) - } - ) - .addMatcher( - api.endpoints.closeTask.matchFulfilled, - (state, { payload }) => { - Object.keys(state).forEach(day => { - state[day] = state[day].filter(task => task.id !== payload.id) - }) - } - ) - .addMatcher( - api.endpoints.fetchSchedule.matchFulfilled, - (state, { payload }) => { - Object.assign(state, payload) - } - ) - } -}) - -export const { updateScheduleTaskPositionIndex } = scheduleSlice.actions -export default scheduleSlice.reducer diff --git a/src/redux/reducers/tasksSlice.ts b/src/redux/reducers/tasksSlice.ts index b6b5ae3..4bfc451 100644 --- a/src/redux/reducers/tasksSlice.ts +++ b/src/redux/reducers/tasksSlice.ts @@ -61,6 +61,19 @@ const tasksSlice = createSlice({ const taskListId = meta.arg.type state.taskLists[taskListId].status = 'FAILED' }) + .addMatcher( + api.endpoints.fetchSchedule.matchFulfilled, + (state, { payload }) => { + Object.entries(payload).forEach(([key, value]) => { + state.taskLists[key] = { + status: 'SUCCEEDED', + totalElements: value.length, + allIds: value.map(it => it.id) + } + value.forEach(it => state.byId[it.id] = it) + }) + } + ) .addMatcher(api.endpoints.createTask.matchFulfilled, (state, { payload }) => { const taskList = state.taskLists[TASK_LIST_ID.INBOX] taskList.totalElements += 1 @@ -87,9 +100,12 @@ const tasksSlice = createSlice({ state.byId[payload.id] = payload }) .addMatcher(api.endpoints.updateTasksOrderAsync.matchPending, (state, { meta }) => { - const { sourceListType, sourceIndex, destinationListType, destinationIndex } = meta.arg.originalArgs - const id = state.taskLists[sourceListType].allIds.splice(sourceIndex, 1)[0] - state.taskLists[destinationListType].allIds.splice(destinationIndex, 0, id) + const { sourceListType, taskId, destinationListType, destinationIndex } = meta.arg.originalArgs + state.taskLists[sourceListType].allIds = state.taskLists[sourceListType].allIds.filter(it => it != taskId) + state.taskLists[destinationListType].allIds.splice(destinationIndex, 0, taskId) + }) + .addMatcher(api.endpoints.updateTasksOrderAsync.matchFulfilled, (state, { payload }) => { + state.byId[payload.id] = payload }) } }) diff --git a/src/redux/store.ts b/src/redux/store.ts index 15946cc..68a60ec 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,13 +1,11 @@ import { configureStore } from '@reduxjs/toolkit' import { api } from './api' -import scheduleReducer from './reducers/scheduleSlice' import tasksReducer from './reducers/tasksSlice' import { REDUX_DEV_TOOLS_ENABLED } from '../config' export const store = configureStore({ reducer: { tasks: tasksReducer, - schedule: scheduleReducer, [api.reducerPath]: api.reducer }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(api.middleware), diff --git a/src/redux/tags.ts b/src/redux/tags.ts new file mode 100644 index 0000000..020b58d --- /dev/null +++ b/src/redux/tags.ts @@ -0,0 +1 @@ +export const SCHEDULE_TAG = 'Schedule' as const \ No newline at end of file diff --git a/src/utils/dragAndDropUtils.ts b/src/utils/dragAndDropUtils.ts index b845c0d..99159b9 100644 --- a/src/utils/dragAndDropUtils.ts +++ b/src/utils/dragAndDropUtils.ts @@ -2,6 +2,3 @@ import { DraggableLocation } from '@hello-pangea/dnd' export const userReallyChangedOrder = (source: DraggableLocation, destination: DraggableLocation | null): boolean => !!destination && (source.droppableId !== destination.droppableId || source.index !== destination.index) - -export const userChangedLists = (source: DraggableLocation, destination: DraggableLocation): boolean => - destination && source.droppableId !== destination.droppableId \ No newline at end of file