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