Skip to content

Commit a2dade9

Browse files
authored
feat: add id generation (#8)
1 parent 271e211 commit a2dade9

File tree

13 files changed

+13309
-6
lines changed

13 files changed

+13309
-6
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,9 @@ it('adds a todo', { rest: { todos: 'todos.json' } }, () => {
6565
You need `cy.then` to access the changed data _after_ Cypress commands have finished. Alternatively, you can wrap the array reference:
6666
6767
```js
68-
cy.get('input.new-todo')
69-
.type('Write tests{enter}')
68+
cy.get('input.new-todo').type('Write tests{enter}')
7069
// there should be one more item in the array
71-
cy.wrap(todos)
72-
.should('have.length', n + 1)
70+
cy.wrap(todos).should('have.length', n + 1)
7371
```
7472
7573
### baseUrl
@@ -85,6 +83,14 @@ describe(
8583
8684
![Base URL option routes](./images/base-url.png)
8785
86+
### assignId
87+
88+
Typically, the backend creates new `id` for each `POST /resource` call that does not send the `id` property. You can create UUID values automatically:
89+
90+
```js
91+
{ rest: { assignId: true, todos: 'todos.json' } }
92+
```
93+
8894
## Small print
8995
9096
Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2025

app-server-id/app.js

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/* global Vue, Vuex, axios */
2+
/* eslint-disable no-console */
3+
/* eslint-disable-next-line */
4+
5+
function appStart() {
6+
Vue.use(Vuex)
7+
8+
const store = new Vuex.Store({
9+
state: {
10+
loading: false,
11+
todos: [],
12+
newTodo: '',
13+
},
14+
getters: {
15+
newTodo: (state) => state.newTodo,
16+
todos: (state) => state.todos,
17+
loading: (state) => state.loading,
18+
},
19+
mutations: {
20+
SET_LOADING(state, flag) {
21+
state.loading = flag
22+
if (flag === false) {
23+
// an easy way for the application to signal
24+
// that it is done loading
25+
document.body.classList.add('loaded')
26+
}
27+
},
28+
SET_TODOS(state, todos) {
29+
state.todos = todos
30+
// expose the todos via the global "window" object
31+
// but only if we are running Cypress tests
32+
if (window.Cypress) {
33+
window.todos = todos
34+
}
35+
},
36+
SET_NEW_TODO(state, todo) {
37+
state.newTodo = todo
38+
},
39+
ADD_TODO(state, todoObject) {
40+
state.todos.push(todoObject)
41+
},
42+
REMOVE_TODO(state, todo) {
43+
let todos = state.todos
44+
todos.splice(todos.indexOf(todo), 1)
45+
},
46+
UPDATE_TODO(state, updatedTodo) {
47+
const index = state.todos.findIndex(
48+
(todo) => todo.id === updatedTodo.id,
49+
)
50+
if (index !== -1) {
51+
state.todos.splice(index, 1, updatedTodo)
52+
}
53+
},
54+
CLEAR_NEW_TODO(state) {
55+
state.newTodo = ''
56+
},
57+
},
58+
actions: {
59+
loadTodos({ commit, state }) {
60+
commit('SET_LOADING', true)
61+
62+
axios
63+
.get('/todos')
64+
.then((r) => r.data)
65+
.then((todos) => {
66+
commit('SET_TODOS', todos)
67+
})
68+
.catch((e) => {
69+
console.error('could not load todos')
70+
console.error(e.message)
71+
console.error(e.response.data)
72+
})
73+
.finally(() => {
74+
commit('SET_LOADING', false)
75+
})
76+
},
77+
78+
/**
79+
* Sets text for the future todo
80+
*
81+
* @param {any} { commit }
82+
* @param {string} todo Message
83+
*/
84+
setNewTodo({ commit }, todo) {
85+
commit('SET_NEW_TODO', todo)
86+
},
87+
addTodo({ commit, state }) {
88+
if (!state.newTodo) {
89+
// do not add empty todos
90+
return
91+
}
92+
const todo = {
93+
title: state.newTodo,
94+
completed: false,
95+
}
96+
axios.post('/todos', todo).then((response) => {
97+
commit('ADD_TODO', response.data)
98+
})
99+
},
100+
addEntireTodo({ commit }, todoFields) {
101+
const todo = {
102+
...todoFields,
103+
}
104+
axios.post('/todos', todo).then((response) => {
105+
commit('ADD_TODO', response.data)
106+
})
107+
},
108+
removeTodo({ commit }, todo) {
109+
axios.delete(`/todos/${todo.id}`).then(() => {
110+
commit('REMOVE_TODO', todo)
111+
})
112+
},
113+
async removeCompleted({ commit, state }) {
114+
const remainingTodos = state.todos.filter(
115+
(todo) => !todo.completed,
116+
)
117+
const completedTodos = state.todos.filter(
118+
(todo) => todo.completed,
119+
)
120+
121+
for (const todo of completedTodos) {
122+
await axios.delete(`/todos/${todo.id}`)
123+
}
124+
commit('SET_TODOS', remainingTodos)
125+
},
126+
updateTodo({ commit }, todo) {
127+
axios.patch(`/todos/${todo.id}`, todo).then(() => {
128+
commit('UPDATE_TODO', todo)
129+
})
130+
},
131+
clearNewTodo({ commit }) {
132+
commit('CLEAR_NEW_TODO')
133+
},
134+
},
135+
})
136+
137+
// a few helper utilities
138+
const filters = {
139+
all: function (todos) {
140+
return todos
141+
},
142+
active: function (todos) {
143+
return todos.filter(function (todo) {
144+
return !todo.completed
145+
})
146+
},
147+
completed: function (todos) {
148+
return todos.filter(function (todo) {
149+
return todo.completed
150+
})
151+
},
152+
}
153+
154+
// app Vue instance
155+
const app = new Vue({
156+
store,
157+
data: {
158+
file: null,
159+
visibility: 'all',
160+
},
161+
el: '.todoapp',
162+
163+
created() {
164+
this.$store.dispatch('loadTodos')
165+
},
166+
167+
// computed properties
168+
// https://vuejs.org/guide/computed.html
169+
computed: {
170+
loading() {
171+
return this.$store.getters.loading
172+
},
173+
newTodo() {
174+
return this.$store.getters.newTodo
175+
},
176+
todos() {
177+
return this.$store.getters.todos
178+
},
179+
filteredTodos() {
180+
return filters[this.visibility](this.$store.getters.todos)
181+
},
182+
remaining() {
183+
return this.$store.getters.todos.filter(
184+
(todo) => !todo.completed,
185+
).length
186+
},
187+
},
188+
189+
// methods that implement data logic.
190+
// note there's no DOM manipulation here at all.
191+
methods: {
192+
pluralize: function (word, count) {
193+
return word + (count === 1 ? '' : 's')
194+
},
195+
196+
setNewTodo(e) {
197+
this.$store.dispatch('setNewTodo', e.target.value)
198+
},
199+
200+
addTodo(e) {
201+
// do not allow adding empty todos
202+
if (!e.target.value.trim()) {
203+
throw new Error('Cannot add a blank todo')
204+
}
205+
e.target.value = ''
206+
this.$store.dispatch('addTodo')
207+
this.$store.dispatch('clearNewTodo')
208+
},
209+
210+
removeTodo(todo) {
211+
this.$store.dispatch('removeTodo', todo)
212+
},
213+
214+
// utility method for create a todo with title and completed state
215+
addEntireTodo(title, completed = false) {
216+
this.$store.dispatch('addEntireTodo', { title, completed })
217+
},
218+
219+
removeCompleted() {
220+
this.$store.dispatch('removeCompleted')
221+
},
222+
},
223+
})
224+
225+
// use the Router from the vendor/director.js library
226+
;(function (app, Router) {
227+
'use strict'
228+
229+
var router = new Router()
230+
231+
;['all', 'active', 'completed'].forEach(function (visibility) {
232+
router.on(visibility, function () {
233+
app.visibility = visibility
234+
})
235+
})
236+
237+
router.configure({
238+
notfound: function () {
239+
window.location.hash = ''
240+
app.visibility = 'all'
241+
},
242+
})
243+
244+
router.init()
245+
})(app, Router)
246+
247+
// if you want to expose "app" globally only
248+
// during end-to-end tests you can guard it using "window.Cypress" flag
249+
// if (window.Cypress) {
250+
window.app = app
251+
// }
252+
}
253+
254+
appStart()

0 commit comments

Comments
 (0)