diff --git a/react/.vscode/settings.json b/react/.vscode/settings.json deleted file mode 100644 index be5c9ca..0000000 --- a/react/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "editor.tabSize": 2, - "editor.insertSpaces": true, - "prettier.eslintIntegration": true -} diff --git a/react/src/actions/lists.js b/react/src/actions/lists.js index 72c1f49..cedfbc8 100644 --- a/react/src/actions/lists.js +++ b/react/src/actions/lists.js @@ -1,4 +1,4 @@ -import { API_ROOT, getToken } from './util'; +import { API_ROOT, getToken, mongoObjectId } from './util'; import { RECIEVE_TODOS } from './todos'; export const ADD_LIST = 'ADD_LIST'; @@ -20,17 +20,9 @@ function requestLists() { function recieveLists(lists) { return { type: RECIEVE_LISTS, lists }; } - -function invalidateLists() { - return { type: INVALIDATE_LISTS }; -} -function validateLists() { - return { type: VALIDATE_LISTS }; -} export function changeList(list) { return { type: CHANGE_LIST, list }; } - export function startCreateList() { return { type: START_CREATE_LIST }; } @@ -46,44 +38,57 @@ export function stopEditList() { export function addList(name) { return async dispatch => { - dispatch(invalidateLists()); - const response = await fetch(`${API_ROOT}/lists`, { - body: JSON.stringify({ name }), - headers: { - Authorization: `Bearer ${getToken()}`, - 'content-type': 'application/json', + const id = mongoObjectId(); + dispatch({ + type: ADD_LIST, + list: { + name, + id, + todos: [], + }, + meta: { + offline: { + effect: { + url: `${API_ROOT}/lists`, + body: JSON.stringify({ name, id }), + headers: { + Authorization: `Bearer ${getToken()}`, + 'content-type': 'application/json', + }, + method: 'POST', + }, + rollback: { + type: INVALIDATE_LISTS, + }, + }, }, - method: 'POST', }); - const json = await response.json(); - const list = json.data; - dispatch({ type: ADD_LIST, list }); - dispatch(changeList(list.id)); - dispatch(validateLists()); }; } export function removeList() { return async (dispatch, getState) => { - let state = getState(); + const state = getState(); const { list } = state.lists; - dispatch(invalidateLists()); - const response = await fetch(`${API_ROOT}/lists/${list}`, { - headers: { - Authorization: `Bearer ${getToken()}`, - 'content-type': 'application/json', + dispatch({ + type: REMOVE_LIST, + list, + meta: { + offline: { + effect: { + url: `${API_ROOT}/lists/${list}`, + headers: { + Authorization: `Bearer ${getToken()}`, + 'content-type': 'application/json', + }, + method: 'DELETE', + }, + rollback: { + type: INVALIDATE_LISTS, + }, + }, }, - method: 'DELETE', }); - const json = await response.json(); - if (json.success) { - dispatch({ type: REMOVE_LIST, list }); - state = getState(); - const lists = Object.values(state.lists.lists); - const newList = lists.length ? lists[lists.length - 1].id : ''; - dispatch(changeList(newList)); - } - dispatch(validateLists()); }; } @@ -91,20 +96,27 @@ export function editList(name) { return async (dispatch, getState) => { const state = getState(); const { list } = state.lists; - dispatch(invalidateLists()); - const response = await fetch(`${API_ROOT}/lists/${list}`, { - body: JSON.stringify({ name }), - headers: { - Authorization: `Bearer ${getToken()}`, - 'content-type': 'application/json', + dispatch({ + type: EDIT_LIST_NAME, + list, + name, + meta: { + offline: { + effect: { + url: `${API_ROOT}/lists/${list}`, + body: JSON.stringify({ name }), + headers: { + Authorization: `Bearer ${getToken()}`, + 'content-type': 'application/json', + }, + method: 'PATCH', + }, + rollback: { + type: INVALIDATE_LISTS, + }, + }, }, - method: 'PATCH', }); - const json = await response.json(); - if (json.success) { - dispatch({ type: EDIT_LIST_NAME, list, name }); - } - dispatch(validateLists()); }; } @@ -131,29 +143,19 @@ export function fetchLists() { }); const json = await response.json(); const lists = json.data; - const listsObj = lists.reduce((obj, list) => { + const listsObj = lists.reduce((obj, curList) => { const newObj = { ...obj }; - newObj[list.id] = { + newObj[curList.id] = { dirty: true, fetching: false, editing: false, - ...list, - todos: list.todos.map(todo => todo.id), + ...curList, + todos: curList.todos.map(todo => todo.id), }; return newObj; }, {}); dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) }); dispatch(recieveLists(listsObj)); - if (lists.length !== 0) { - dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id)); - } - }; -} - -export function loadLists() { - return async dispatch => { - dispatch(requestLists()); - dispatch(fetchLists()); }; } diff --git a/react/src/actions/todos.js b/react/src/actions/todos.js index 0380803..c72d972 100644 --- a/react/src/actions/todos.js +++ b/react/src/actions/todos.js @@ -1,5 +1,5 @@ -import { API_ROOT, getToken } from './util'; -import { fetchLists, INVALIDATE_LISTS } from './lists'; +import { API_ROOT, getToken, mongoObjectId } from './util'; +import { INVALIDATE_LISTS } from './lists'; export const ADD_TODO = 'ADD_TODO'; export const REMOVE_TODO = 'REMOVE_TODO'; @@ -21,13 +21,6 @@ export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter }; } -function invalidateTodos() { - return { type: INVALIDATE_TODOS }; -} -function validateTodos() { - return { type: VALIDATE_TODOS }; -} - export function fetchTodos() { return async dispatch => { dispatch({ type: REQUEST_TODOS }); @@ -46,24 +39,32 @@ export function addTodo(text) { return async (dispatch, getState) => { const state = getState(); const { list } = state.lists; + const id = mongoObjectId(); if (list) { - dispatch(invalidateTodos()); - const response = await fetch(`${API_ROOT}/lists/${list}/todos`, { - body: JSON.stringify({ text }), - headers: { - Authorization: `Bearer ${getToken()}`, - 'content-type': 'application/json', + dispatch({ + type: ADD_TODO, + todo: { + text, + id, + completed: false, + }, + meta: { + offline: { + effect: { + url: `${API_ROOT}/lists/${list}/todos`, + body: JSON.stringify({ text, id }), + headers: { + Authorization: `Bearer ${getToken()}`, + 'content-type': 'application/json', + }, + method: 'POST', + }, + rollback: { + type: INVALIDATE_LISTS, + }, + }, }, - method: 'POST', }); - const json = await response.json(); - const todo = json.data; - if (json.success) { - dispatch({ type: ADD_TODO, todo }); - } else { - dispatch(fetchLists()); - } - dispatch(validateTodos()); } }; } diff --git a/react/src/actions/user.js b/react/src/actions/user.js index aec1b56..767b351 100644 --- a/react/src/actions/user.js +++ b/react/src/actions/user.js @@ -1,5 +1,5 @@ import { API_ROOT, getToken, setToken } from './util'; -import { loadLists } from './lists'; +import { fetchLists } from './lists'; export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; export const LOGIN_FAIL = 'LOGIN_FAIL'; @@ -40,7 +40,7 @@ export function loadUser() { const json = await response.json(); if (json.success) { dispatch(loginSuccess(json.data)); - dispatch(loadLists()); + dispatch(fetchLists()); } else { dispatch(loginFail(json.error)); } @@ -64,7 +64,7 @@ export function login(user) { if (json.success) { setToken(json.data.jwt); dispatch(loginSuccess(json.data)); - dispatch(loadLists()); + dispatch(fetchLists()); } else { dispatch(loginFail(json.error)); } @@ -93,7 +93,7 @@ export function signup(user) { if (json.success) { setToken(json.data.jwt); dispatch(signupSuccess(json.data)); - dispatch(loadLists()); + dispatch(fetchLists()); } else { dispatch(signupFail(json.error)); } diff --git a/react/src/actions/util.js b/react/src/actions/util.js index 46b55c8..2d436e9 100644 --- a/react/src/actions/util.js +++ b/react/src/actions/util.js @@ -9,3 +9,15 @@ export function setToken(_token) { export function getToken() { return token; } + +export function mongoObjectId() { + // eslint-disable-next-line + const timestamp = ((new Date().getTime() / 1000) | 0).toString(16); + return ( + timestamp + + 'xxxxxxxxxxxxxxxx' + // eslint-disable-next-line + .replace(/[x]/g, () => ((Math.random() * 16) | 0).toString(16)) + .toLowerCase() + ); +} diff --git a/react/src/reducers/lists.js b/react/src/reducers/lists.js index 205e61d..f91a430 100644 --- a/react/src/reducers/lists.js +++ b/react/src/reducers/lists.js @@ -40,14 +40,23 @@ export default function lists( }; case CHANGE_LIST: return { ...state, list: action.list }; - case RECIEVE_LISTS: + case RECIEVE_LISTS: { + const newLists = Object.values(action.lists); + let { list } = state; + if (newLists.length !== 0) { + if (!newLists.some(curList => curList.id === list)) { + list = newLists[0].id; + } + } return { ...state, dirty: false, loaded: true, fetching: false, lists: action.lists, + list, }; + } case START_CREATE_LIST: return { ...state, @@ -63,13 +72,16 @@ export default function lists( ...state, creating: false, lists: { ...state.lists, [action.list.id]: action.list }, + list: action.list.id, }; case REMOVE_LIST: { const newLists = { ...state.lists }; delete newLists[action.list]; + const listsObjs = Object.values(newLists); + const list = listsObjs.length ? listsObjs[listsObjs.length - 1].id : ''; return { ...state, - list: null, + list, lists: newLists, }; } diff --git a/routes/lists.js b/routes/lists.js index ac3d511..f11c527 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -24,7 +24,8 @@ router.post( '/', asyncHelper(async (req, res) => { const { name } = req.body; - const newList = new TodoList({ name, user: req.user.id }); + const { id } = req.body || mongoose.Types.ObjectId(); + const newList = new TodoList({ name, user: req.user.id, _id: id }); await newList.save(); res.json({ success: true, data: newList.toJson() }); }), diff --git a/routes/todos.js b/routes/todos.js index 55f945f..bc2d1c8 100644 --- a/routes/todos.js +++ b/routes/todos.js @@ -26,7 +26,8 @@ router.post( asyncHelper(async (req, res) => { const { listId } = res.locals || req.body; const { text } = req.body; - const todo = new Todo({ text, list: listId, user: req.user.id }); + const { id } = req.body || mongoose.Types.ObjectId(); + const todo = new Todo({ text, list: listId, user: req.user.id, _id: id }); await todo.save(); res.json({ success: true, data: todo.toJson() }); }), diff --git a/tests/integration/lists.test.js b/tests/integration/lists.test.js index 65cfb39..d90dbaf 100644 --- a/tests/integration/lists.test.js +++ b/tests/integration/lists.test.js @@ -71,6 +71,25 @@ describe('test lists', () => { const freshUser = await User.findById(user.id).exec(); expect(freshUser.lists).toContain(response.body.data.id); }); + test('should create list with custom id', async () => { + const id = mongoose.Types.ObjectId(); + const response = await request(server) + .post('/api/lists') + .send({ + name: 'List2', + id, + }) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8'); + expect(response.body.success).toBeTruthy(); + expect(await TodoList.findOne({ name: 'List2', _id: id })).toBeTruthy(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.lists).toContain(response.body.data.id); + }); + test('should not create list without authentication', async () => { await request(server) .post('/api/lists') diff --git a/tests/integration/todos.test.js b/tests/integration/todos.test.js index a3b2f29..4f05b5f 100644 --- a/tests/integration/todos.test.js +++ b/tests/integration/todos.test.js @@ -84,6 +84,28 @@ describe('test todos', () => { const freshList = await TodoList.findById(list.id).exec(); expect(freshList.todos).toContain(response.body.data.id); }); + test('should create todo with custom id', async () => { + const id = mongoose.Types.ObjectId(); + const response = await request(server) + .post(`/api/lists/${list._id}/todos`) + .send({ + text: 'Todo2', + id, + }) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8'); + expect(response.body.success).toBeTruthy(); + expect( + await Todo.findOne({ text: 'Todo2', list: list._id, _id: id }), + ).toBeTruthy(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.todos).toContain(response.body.data.id); + const freshList = await TodoList.findById(list.id).exec(); + expect(freshList.todos).toContain(response.body.data.id); + }); test('should not create todo without authentication', async () => { await request(server) .post(`/api/lists/${list._id}/todos`)