Skip to content

Commit f872f9c

Browse files
Merge pull request #528 from hamiltoes/feature/instance-status
Feature: per-instance pending status
2 parents b0c6c9f + c620bba commit f872f9c

17 files changed

+577
-29
lines changed

docs/model-classes.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ const todo = new Todo({ description: 'Do something!' })
339339

340340
The examples above show instantiating a new Model instance without an `id` field. In this case, the record is not added to the Vuex store. If you instantiate a record **with an `id`** field, it **will** get added to the Vuex store. *Note: This field is customizable using the `idField` option for this service.*
341341

342-
Now that we have Model instances, let's take a look at the functionality they provide. Each instance will include the following methods:
342+
Now that we have Model instances, let's take a look at the functionality they provide.
343+
344+
Each instance will include the following methods:
343345

344346
- `.save()`
345347
- `.create()`
@@ -349,6 +351,15 @@ Now that we have Model instances, let's take a look at the functionality they pr
349351
- `.commit()`
350352
- `.reset()`
351353

354+
and the following readonly attributes:
355+
356+
- `isCreatePending` - `create` is currently pending on this model
357+
- `isUpdatePending` - `update` is currently pending on this model
358+
- `isPatchPending` - `patch` is currently pending on this model
359+
- `isRemovePending` - `remove` is currently pending on this model
360+
- `isSavePending` - Any of `create`, `update` or `patch` is currently pending on this model
361+
- `isPending` - Any method is currently pending on this model
362+
352363
*Remember, if a record already has an attribute with any of these method names, it will be overwritten with the method.*
353364

354365
These methods give access to many of the store `actions` and `mutations`. Using Model instances, you no longer have to use `mapActions` for `create`, `patch`, `update`, or `remove`. You also no longer have to use `mapMutations` for `createCopy`, `commitCopy`, or `resetCopy`.

docs/service-plugin.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ Each service comes loaded with the following default state:
156156
errorOnCreate: undefined,
157157
errorOnUpdate: undefined,
158158
errorOnPatch: undefined,
159-
errorOnRemove: undefined
159+
errorOnRemove: undefined,
160+
161+
isIdCreatePending: [],
162+
isIdUpdatePending: [],
163+
isIdPatchPending: [],
164+
isIdRemovePending: [],
160165
}
161166
```
162167

@@ -193,6 +198,13 @@ The following state attribute will be populated with any request error, serializ
193198
- `errorOnPatch {Error}`
194199
- `errorOnRemo {Error}`
195200

201+
The following state attributes allow you to bind to the pending state of requests *per item ID*
202+
203+
- `isIdCreatePending {Array}` - Contains `id` if there's a pending `create` request for `id`.
204+
- `isIdUpdatePending {Array}` -Contains `id` if there's a pending `update` request for `id`.
205+
- `isIdPatchPending {Array}` - Contains `id` if there's a pending `patch` request for `id`.
206+
- `isIdRemovePending {Array}` - Contains `id` if there's a pending `remove` request for `id`.
207+
196208
## Service Getters
197209

198210
Service modules include the following getters:
@@ -206,6 +218,15 @@ Service modules include the following getters:
206218
- `id {Number|String}` - the id of the data to be retrieved by id from the store.
207219
- `params {Object}` - an object containing a Feathers `query` object.
208220

221+
The following getters ease access to per-instance pending status
222+
223+
- `isCreatePendingById(id) {Function}` - Check if `create` is pending for `id`
224+
- `isUpdatePendingById(id) {Function}` - Check if `update` is pending for `id`
225+
- `isPatchPendingById(id) {Function}` - Check if `patch` is pending for `id`
226+
- `isRemovePendingById(id) {Function}` - Check if `remove` is pending for `id`
227+
- `isSavePendingById(id) {Function}` - Check if `create`, `update`, or `patch` is pending for `id`
228+
- `isPendingById(id) {Function}` - Check if `create`, `update`, `patch` or `remove` is pending for `id`
229+
209230
## Service Mutations
210231

211232
The following mutations are included in each service module.
@@ -262,7 +283,9 @@ Clears all data from `ids`, `keyedById`, and `currentId`
262283
The following mutations are called automatically by the service actions, and will rarely, if ever, need to be used manually.
263284

264285
- `setPending(state, method)` - sets the `is${method}Pending` attribute to true
286+
- `setIdPending(state, { method, id })` - adds `id` to `isId${method}Pending` array
265287
- `unsetPending(state, method)` - sets the `is${method}Pending` attribute to false
288+
- `unsetIdPending(state, { method, id })` - removes `id` from `isId${method}Pending` array
266289

267290
### Mutations for Managing Errors
268291

src/service-module/make-base-model.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import _get from 'lodash/get'
2121
import { EventEmitter } from 'events'
2222
import { ModelSetupContext } from './types'
2323
import { Store } from 'vuex'
24+
import { GetterName } from './service-module.getters'
2425

2526
const defaultOptions = {
2627
clone: false,
@@ -175,6 +176,37 @@ export default function makeBaseModel(options: FeathersVuexOptions) {
175176
return this
176177
}
177178

179+
/**
180+
* Calls `getter`, passing this model's ID as the parameter
181+
* @param getter name of getter to call
182+
*/
183+
private getGetterWithId(getter: GetterName): unknown {
184+
const { _getters, idField, tempIdField } = this
185+
.constructor as typeof BaseModel
186+
const id =
187+
getId(this, idField) != null ? getId(this, idField) : this[tempIdField]
188+
return _getters.call(this.constructor, getter, id)
189+
}
190+
191+
get isCreatePending(): boolean {
192+
return this.getGetterWithId('isCreatePendingById') as boolean
193+
}
194+
get isUpdatePending(): boolean {
195+
return this.getGetterWithId('isUpdatePendingById') as boolean
196+
}
197+
get isPatchPending(): boolean {
198+
return this.getGetterWithId('isPatchPendingById') as boolean
199+
}
200+
get isRemovePending(): boolean {
201+
return this.getGetterWithId('isRemovePendingById') as boolean
202+
}
203+
get isSavePending(): boolean {
204+
return this.getGetterWithId('isSavePendingById') as boolean
205+
}
206+
get isPending(): boolean {
207+
return this.getGetterWithId('isPendingById') as boolean
208+
}
209+
178210
public static getId(record: Record<string, any>): string {
179211
const { idField } = this.constructor as typeof BaseModel
180212
return getId(record, idField)
@@ -214,7 +246,7 @@ export default function makeBaseModel(options: FeathersVuexOptions) {
214246
* @param method the vuex getter name without the namespace
215247
* @param payload if provided, the getter will be called as a function
216248
*/
217-
public static _getters(name: string, idOrParams?: any, params?: any) {
249+
public static _getters(name: GetterName, idOrParams?: any, params?: any) {
218250
const { namespace, store } = this
219251

220252
if (checkNamespace(namespace, this, options.debug)) {

src/service-module/service-module.actions.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default function makeServiceActions(service: Service<any>) {
9999
params = params || {}
100100

101101
commit('setPending', 'create')
102+
commit('setIdPending', { method: 'create', id: tempIds })
102103

103104
return service
104105
.create(data, params)
@@ -121,34 +122,39 @@ export default function makeServiceActions(service: Service<any>) {
121122

122123
// response = state.keyedById[id]
123124
}
124-
commit('unsetPending', 'create')
125125
commit('removeTemps', tempIds)
126126
return response
127127
})
128128
.catch(error => {
129129
commit('setError', { method: 'create', error })
130-
commit('unsetPending', 'create')
131130
return Promise.reject(error)
132131
})
132+
.finally(() => {
133+
commit('unsetPending', 'create')
134+
commit('unsetIdPending', { method: 'create', id: tempIds })
135+
})
133136
},
134137

135138
update({ commit, dispatch, state }, [id, data, params]) {
136139
commit('setPending', 'update')
140+
commit('setIdPending', { method: 'update', id })
137141

138142
params = fastCopy(params)
139143

140144
return service
141145
.update(id, data, params)
142146
.then(async function (item) {
143147
dispatch('addOrUpdate', item)
144-
commit('unsetPending', 'update')
145148
return state.keyedById[id]
146149
})
147150
.catch(error => {
148151
commit('setError', { method: 'update', error })
149-
commit('unsetPending', 'update')
150152
return Promise.reject(error)
151153
})
154+
.finally(() => {
155+
commit('unsetPending', 'update')
156+
commit('unsetIdPending', { method: 'update', id })
157+
})
152158
},
153159

154160
/**
@@ -157,6 +163,7 @@ export default function makeServiceActions(service: Service<any>) {
157163
*/
158164
patch({ commit, dispatch, state }, [id, data, params]) {
159165
commit('setPending', 'patch')
166+
commit('setIdPending', { method: 'patch', id })
160167

161168
params = fastCopy(params)
162169

@@ -171,14 +178,16 @@ export default function makeServiceActions(service: Service<any>) {
171178
.patch(id, data, params)
172179
.then(async function (item) {
173180
dispatch('addOrUpdate', item)
174-
commit('unsetPending', 'patch')
175181
return state.keyedById[id]
176182
})
177183
.catch(error => {
178184
commit('setError', { method: 'patch', error })
179-
commit('unsetPending', 'patch')
180185
return Promise.reject(error)
181186
})
187+
.finally(() => {
188+
commit('unsetPending', 'patch')
189+
commit('unsetIdPending', { method: 'patch', id })
190+
})
182191
},
183192

184193
remove({ commit }, idOrArray) {
@@ -196,19 +205,22 @@ export default function makeServiceActions(service: Service<any>) {
196205
params = fastCopy(params)
197206

198207
commit('setPending', 'remove')
208+
commit('setIdPending', { method: 'remove', id })
199209

200210
return service
201211
.remove(id, params)
202212
.then(item => {
203213
commit('removeItem', id)
204-
commit('unsetPending', 'remove')
205214
return item
206215
})
207216
.catch(error => {
208217
commit('setError', { method: 'remove', error })
209-
commit('unsetPending', 'remove')
210218
return Promise.reject(error)
211219
})
220+
.finally(() => {
221+
commit('unsetPending', 'remove')
222+
commit('unsetIdPending', { method: 'remove', id })
223+
})
212224
}
213225
}
214226

src/service-module/service-module.getters.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { globalModels as models } from './global-models'
1010
import _get from 'lodash/get'
1111
import _omit from 'lodash/omit'
1212
import { isRef } from '@vue/composition-api'
13+
import { ServiceState } from '..'
14+
import { Id } from '@feathersjs/feathers'
1315

1416
const FILTERS = ['$sort', '$limit', '$skip', '$select']
1517
const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$or']
@@ -114,6 +116,24 @@ export default function makeServiceGetters() {
114116

115117
return Model.copiesById[id]
116118
}
117-
}
119+
},
120+
121+
isCreatePendingById: ({ isIdCreatePending }: ServiceState) => (id: Id) =>
122+
isIdCreatePending.includes(id),
123+
isUpdatePendingById: ({ isIdUpdatePending }: ServiceState) => (id: Id) =>
124+
isIdUpdatePending.includes(id),
125+
isPatchPendingById: ({ isIdPatchPending }: ServiceState) => (id: Id) =>
126+
isIdPatchPending.includes(id),
127+
isRemovePendingById: ({ isIdRemovePending }: ServiceState) => (id: Id) =>
128+
isIdRemovePending.includes(id),
129+
isSavePendingById: (state: ServiceState, getters) => (id: Id) =>
130+
getters.isCreatePendingById(id) ||
131+
getters.isUpdatePendingById(id) ||
132+
getters.isPatchPendingById(id),
133+
isPendingById: (state: ServiceState, getters) => (id: Id) =>
134+
getters.isSavePendingById(id) ||
135+
getters.isRemovePendingById(id)
118136
}
119137
}
138+
139+
export type GetterName = keyof ReturnType<typeof makeServiceGetters>

src/service-module/service-module.mutations.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ import { globalModels as models } from './global-models'
1616
import _omit from 'lodash/omit'
1717
import _get from 'lodash/get'
1818
import _isObject from 'lodash/isObject'
19+
import { Id } from '@feathersjs/feathers'
20+
import { ServiceState } from '..'
21+
22+
export type PendingServiceMethodName =
23+
| 'find'
24+
| 'get'
25+
| 'create'
26+
| 'update'
27+
| 'patch'
28+
| 'remove'
29+
export type PendingIdServiceMethodName = Exclude<
30+
PendingServiceMethodName,
31+
'find' | 'get'
32+
>
1933

2034
export default function makeServiceMutations() {
2135
function addItems(state, items) {
@@ -374,24 +388,50 @@ export default function makeServiceMutations() {
374388
Vue.set(state.pagination, qid, newState)
375389
},
376390

377-
setPending(state, method: string): void {
391+
setPending(state, method: PendingServiceMethodName): void {
378392
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
379393
state[`is${uppercaseMethod}Pending`] = true
380394
},
381-
unsetPending(state, method: string): void {
395+
unsetPending(state, method: PendingServiceMethodName): void {
382396
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
383397
state[`is${uppercaseMethod}Pending`] = false
384398
},
385399

386-
setError(state, payload: { method: string; error: Error }): void {
400+
setIdPending(state, payload: { method: PendingIdServiceMethodName, id: Id | Id[] }): void {
401+
const { method, id } = payload
402+
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
403+
const isIdMethodPending = state[`isId${uppercaseMethod}Pending`] as ServiceState['isIdCreatePending']
404+
// if `id` is an array, ensure it doesn't have duplicates
405+
const ids = Array.isArray(id) ? [...new Set(id)] : [id]
406+
ids.forEach(id => {
407+
if (typeof id === 'number' || typeof id === 'string') {
408+
isIdMethodPending.push(id)
409+
}
410+
})
411+
},
412+
unsetIdPending(state, payload: { method: PendingIdServiceMethodName, id: Id | Id[] }): void {
413+
const { method, id } = payload
414+
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
415+
const isIdMethodPending = state[`isId${uppercaseMethod}Pending`] as ServiceState['isIdCreatePending']
416+
// if `id` is an array, ensure it doesn't have duplicates
417+
const ids = Array.isArray(id) ? [...new Set(id)] : [id]
418+
ids.forEach(id => {
419+
const idx = isIdMethodPending.indexOf(id)
420+
if (idx >= 0) {
421+
Vue.delete(isIdMethodPending, idx)
422+
}
423+
})
424+
},
425+
426+
setError(state, payload: { method: PendingServiceMethodName; error: Error }): void {
387427
const { method, error } = payload
388428
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
389429
state[`errorOn${uppercaseMethod}`] = Object.assign(
390430
{},
391431
serializeError(error)
392432
)
393433
},
394-
clearError(state, method: string): void {
434+
clearError(state, method: PendingServiceMethodName): void {
395435
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
396436
state[`errorOn${uppercaseMethod}`] = null
397437
}

src/service-module/service-module.state.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ eslint
77
import _omit from 'lodash/omit'
88

99
import { MakeServicePluginOptions, Model } from './types'
10+
import { Id } from '@feathersjs/feathers'
1011

1112
export interface ServiceStateExclusiveDefaults {
1213
ids: string[]
@@ -36,6 +37,11 @@ export interface ServiceStateExclusiveDefaults {
3637
}
3738
paramsForServer: string[]
3839
modelName?: string
40+
41+
isIdCreatePending: Id[]
42+
isIdUpdatePending: Id[]
43+
isIdPatchPending: Id[]
44+
isIdRemovePending: Id[]
3945
}
4046

4147
export interface ServiceState<M extends Model = Model> {
@@ -77,6 +83,10 @@ export interface ServiceState<M extends Model = Model> {
7783
default?: PaginationState
7884
}
7985
modelName?: string
86+
isIdCreatePending: Id[]
87+
isIdUpdatePending: Id[]
88+
isIdPatchPending: Id[]
89+
isIdRemovePending: Id[]
8090
}
8191

8292
export interface PaginationState {
@@ -124,7 +134,12 @@ export default function makeDefaultState(options: MakeServicePluginOptions) {
124134
errorOnCreate: null,
125135
errorOnUpdate: null,
126136
errorOnPatch: null,
127-
errorOnRemove: null
137+
errorOnRemove: null,
138+
139+
isIdCreatePending: [],
140+
isIdUpdatePending: [],
141+
isIdPatchPending: [],
142+
isIdRemovePending: [],
128143
}
129144

130145
if (options.Model) {

0 commit comments

Comments
 (0)