mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 23:57:49 +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';
|
import { RECIEVE_TODOS } from './todos';
|
||||||
|
|
||||||
export const ADD_LIST = 'ADD_LIST';
|
export const ADD_LIST = 'ADD_LIST';
|
||||||
@@ -20,17 +20,9 @@ function requestLists() {
|
|||||||
function recieveLists(lists) {
|
function recieveLists(lists) {
|
||||||
return { type: RECIEVE_LISTS, lists };
|
return { type: RECIEVE_LISTS, lists };
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateLists() {
|
|
||||||
return { type: INVALIDATE_LISTS };
|
|
||||||
}
|
|
||||||
function validateLists() {
|
|
||||||
return { type: VALIDATE_LISTS };
|
|
||||||
}
|
|
||||||
export function changeList(list) {
|
export function changeList(list) {
|
||||||
return { type: CHANGE_LIST, list };
|
return { type: CHANGE_LIST, list };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startCreateList() {
|
export function startCreateList() {
|
||||||
return { type: START_CREATE_LIST };
|
return { type: START_CREATE_LIST };
|
||||||
}
|
}
|
||||||
@@ -46,44 +38,57 @@ export function stopEditList() {
|
|||||||
|
|
||||||
export function addList(name) {
|
export function addList(name) {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
dispatch(invalidateLists());
|
const id = mongoObjectId();
|
||||||
const response = await fetch(`${API_ROOT}/lists`, {
|
dispatch({
|
||||||
body: JSON.stringify({ name }),
|
type: ADD_LIST,
|
||||||
headers: {
|
list: {
|
||||||
Authorization: `Bearer ${getToken()}`,
|
name,
|
||||||
'content-type': 'application/json',
|
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() {
|
export function removeList() {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
let state = getState();
|
const state = getState();
|
||||||
const { list } = state.lists;
|
const { list } = state.lists;
|
||||||
dispatch(invalidateLists());
|
dispatch({
|
||||||
const response = await fetch(`${API_ROOT}/lists/${list}`, {
|
type: REMOVE_LIST,
|
||||||
headers: {
|
list,
|
||||||
Authorization: `Bearer ${getToken()}`,
|
meta: {
|
||||||
'content-type': 'application/json',
|
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) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { list } = state.lists;
|
const { list } = state.lists;
|
||||||
dispatch(invalidateLists());
|
dispatch({
|
||||||
const response = await fetch(`${API_ROOT}/lists/${list}`, {
|
type: EDIT_LIST_NAME,
|
||||||
body: JSON.stringify({ name }),
|
list,
|
||||||
headers: {
|
name,
|
||||||
Authorization: `Bearer ${getToken()}`,
|
meta: {
|
||||||
'content-type': 'application/json',
|
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 json = await response.json();
|
||||||
const lists = json.data;
|
const lists = json.data;
|
||||||
const listsObj = lists.reduce((obj, list) => {
|
const listsObj = lists.reduce((obj, curList) => {
|
||||||
const newObj = { ...obj };
|
const newObj = { ...obj };
|
||||||
newObj[list.id] = {
|
newObj[curList.id] = {
|
||||||
dirty: true,
|
dirty: true,
|
||||||
fetching: false,
|
fetching: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
...list,
|
...curList,
|
||||||
todos: list.todos.map(todo => todo.id),
|
todos: curList.todos.map(todo => todo.id),
|
||||||
};
|
};
|
||||||
return newObj;
|
return newObj;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
|
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
|
||||||
dispatch(recieveLists(listsObj));
|
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 { API_ROOT, getToken, mongoObjectId } from './util';
|
||||||
import { fetchLists, INVALIDATE_LISTS } from './lists';
|
import { INVALIDATE_LISTS } from './lists';
|
||||||
|
|
||||||
export const ADD_TODO = 'ADD_TODO';
|
export const ADD_TODO = 'ADD_TODO';
|
||||||
export const REMOVE_TODO = 'REMOVE_TODO';
|
export const REMOVE_TODO = 'REMOVE_TODO';
|
||||||
@@ -21,13 +21,6 @@ export function setVisibilityFilter(filter) {
|
|||||||
return { type: SET_VISIBILITY_FILTER, filter };
|
return { type: SET_VISIBILITY_FILTER, filter };
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateTodos() {
|
|
||||||
return { type: INVALIDATE_TODOS };
|
|
||||||
}
|
|
||||||
function validateTodos() {
|
|
||||||
return { type: VALIDATE_TODOS };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchTodos() {
|
export function fetchTodos() {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
dispatch({ type: REQUEST_TODOS });
|
dispatch({ type: REQUEST_TODOS });
|
||||||
@@ -46,24 +39,32 @@ export function addTodo(text) {
|
|||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { list } = state.lists;
|
const { list } = state.lists;
|
||||||
|
const id = mongoObjectId();
|
||||||
if (list) {
|
if (list) {
|
||||||
dispatch(invalidateTodos());
|
dispatch({
|
||||||
const response = await fetch(`${API_ROOT}/lists/${list}/todos`, {
|
type: ADD_TODO,
|
||||||
body: JSON.stringify({ text }),
|
todo: {
|
||||||
headers: {
|
text,
|
||||||
Authorization: `Bearer ${getToken()}`,
|
id,
|
||||||
'content-type': 'application/json',
|
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 { API_ROOT, getToken, setToken } from './util';
|
||||||
import { loadLists } from './lists';
|
import { fetchLists } from './lists';
|
||||||
|
|
||||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||||
export const LOGIN_FAIL = 'LOGIN_FAIL';
|
export const LOGIN_FAIL = 'LOGIN_FAIL';
|
||||||
@@ -40,7 +40,7 @@ export function loadUser() {
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
dispatch(loginSuccess(json.data));
|
dispatch(loginSuccess(json.data));
|
||||||
dispatch(loadLists());
|
dispatch(fetchLists());
|
||||||
} else {
|
} else {
|
||||||
dispatch(loginFail(json.error));
|
dispatch(loginFail(json.error));
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export function login(user) {
|
|||||||
if (json.success) {
|
if (json.success) {
|
||||||
setToken(json.data.jwt);
|
setToken(json.data.jwt);
|
||||||
dispatch(loginSuccess(json.data));
|
dispatch(loginSuccess(json.data));
|
||||||
dispatch(loadLists());
|
dispatch(fetchLists());
|
||||||
} else {
|
} else {
|
||||||
dispatch(loginFail(json.error));
|
dispatch(loginFail(json.error));
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ export function signup(user) {
|
|||||||
if (json.success) {
|
if (json.success) {
|
||||||
setToken(json.data.jwt);
|
setToken(json.data.jwt);
|
||||||
dispatch(signupSuccess(json.data));
|
dispatch(signupSuccess(json.data));
|
||||||
dispatch(loadLists());
|
dispatch(fetchLists());
|
||||||
} else {
|
} else {
|
||||||
dispatch(signupFail(json.error));
|
dispatch(signupFail(json.error));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,15 @@ export function setToken(_token) {
|
|||||||
export function getToken() {
|
export function getToken() {
|
||||||
return token;
|
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:
|
case CHANGE_LIST:
|
||||||
return { ...state, list: action.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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
fetching: false,
|
fetching: false,
|
||||||
lists: action.lists,
|
lists: action.lists,
|
||||||
|
list,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case START_CREATE_LIST:
|
case START_CREATE_LIST:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -63,13 +72,16 @@ export default function lists(
|
|||||||
...state,
|
...state,
|
||||||
creating: false,
|
creating: false,
|
||||||
lists: { ...state.lists, [action.list.id]: action.list },
|
lists: { ...state.lists, [action.list.id]: action.list },
|
||||||
|
list: action.list.id,
|
||||||
};
|
};
|
||||||
case REMOVE_LIST: {
|
case REMOVE_LIST: {
|
||||||
const newLists = { ...state.lists };
|
const newLists = { ...state.lists };
|
||||||
delete newLists[action.list];
|
delete newLists[action.list];
|
||||||
|
const listsObjs = Object.values(newLists);
|
||||||
|
const list = listsObjs.length ? listsObjs[listsObjs.length - 1].id : '';
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
list: null,
|
list,
|
||||||
lists: newLists,
|
lists: newLists,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ router.post(
|
|||||||
'/',
|
'/',
|
||||||
asyncHelper(async (req, res) => {
|
asyncHelper(async (req, res) => {
|
||||||
const { name } = req.body;
|
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();
|
await newList.save();
|
||||||
res.json({ success: true, data: newList.toJson() });
|
res.json({ success: true, data: newList.toJson() });
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ router.post(
|
|||||||
asyncHelper(async (req, res) => {
|
asyncHelper(async (req, res) => {
|
||||||
const { listId } = res.locals || req.body;
|
const { listId } = res.locals || req.body;
|
||||||
const { text } = 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();
|
await todo.save();
|
||||||
res.json({ success: true, data: todo.toJson() });
|
res.json({ success: true, data: todo.toJson() });
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -71,6 +71,25 @@ describe('test lists', () => {
|
|||||||
const freshUser = await User.findById(user.id).exec();
|
const freshUser = await User.findById(user.id).exec();
|
||||||
expect(freshUser.lists).toContain(response.body.data.id);
|
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 () => {
|
test('should not create list without authentication', async () => {
|
||||||
await request(server)
|
await request(server)
|
||||||
.post('/api/lists')
|
.post('/api/lists')
|
||||||
|
|||||||
@@ -84,6 +84,28 @@ describe('test todos', () => {
|
|||||||
const freshList = await TodoList.findById(list.id).exec();
|
const freshList = await TodoList.findById(list.id).exec();
|
||||||
expect(freshList.todos).toContain(response.body.data.id);
|
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 () => {
|
test('should not create todo without authentication', async () => {
|
||||||
await request(server)
|
await request(server)
|
||||||
.post(`/api/lists/${list._id}/todos`)
|
.post(`/api/lists/${list._id}/todos`)
|
||||||
|
|||||||
Reference in New Issue
Block a user