mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 15:47:48 +01:00
add offline support
This commit is contained in:
5
react/.vscode/settings.json
vendored
5
react/.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"prettier.eslintIntegration": true
|
||||
}
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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() });
|
||||
}),
|
||||
|
||||
@@ -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() });
|
||||
}),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user