@feardread/feature-factory 5.0.8 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.min.js +2 -2
- package/dist/index.esm.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/rollup.config.mjs +3 -63
- package/src/factory/api.js +99 -23
- package/src/factory/cache.js +70 -71
- package/src/factory/index.js +119 -73
- package/src/factory/thunk.js +3 -3
- package/src/factory/utils.js +355 -0
package/src/factory/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { createSlice, createEntityAdapter, combineReducers } from '@reduxjs/toolkit';
|
|
2
2
|
import ThunkFactory from './thunk';
|
|
3
3
|
import StateFactory from './state';
|
|
4
|
+
import UtilsFactory from './utils';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Creates a Redux feature with standardized structure including slice, async actions, and reducers
|
|
7
8
|
* @param {string} entity - The entity name for the feature
|
|
8
|
-
* @param {Object}
|
|
9
|
+
* @param {Object} customReducers - Custom reducers to include in the slice
|
|
9
10
|
* @param {Object|null} endpoints - API endpoints (currently unused)
|
|
10
11
|
* @returns {Object} Feature factory instance with methods to create slices and manage reducers
|
|
11
12
|
*/
|
|
12
|
-
export function FeatureFactory(entity,
|
|
13
|
+
export function FeatureFactory(entity, customReducers = {}, endpoints = null) {
|
|
13
14
|
if (!entity || typeof entity !== 'string') {
|
|
14
15
|
throw new Error('Entity name must be provided as a string');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const adapter = createEntityAdapter();
|
|
19
|
+
const commonReducers = UtilsFactory.createReducers(entity, adapter);
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Creates a dynamic reducer manager for adding/removing reducers at runtime
|
|
21
|
-
* @param {Object} initialReducers - Initial set of reducers
|
|
22
|
-
* @returns {Object} Reducer manager with methods to manipulate reducers
|
|
23
23
|
*/
|
|
24
24
|
const createReducerManager = (initialReducers = {}) => {
|
|
25
25
|
const reducersMap = { ...initialReducers };
|
|
@@ -27,7 +27,7 @@ export function FeatureFactory(entity, reducers = {}, endpoints = null) {
|
|
|
27
27
|
|
|
28
28
|
return {
|
|
29
29
|
reduce: (state, action) => combinedReducer(state, action),
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
add: (key, reducer) => {
|
|
32
32
|
if (!key || typeof key !== 'string') {
|
|
33
33
|
console.warn('Invalid reducer key provided');
|
|
@@ -37,36 +37,33 @@ export function FeatureFactory(entity, reducers = {}, endpoints = null) {
|
|
|
37
37
|
console.warn(`Reducer with key '${key}' already exists`);
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
reducersMap[key] = reducer;
|
|
42
42
|
combinedReducer = combineReducers(reducersMap);
|
|
43
43
|
},
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
remove: (key) => {
|
|
46
46
|
if (!key || !reducersMap[key]) {
|
|
47
47
|
console.warn(`Reducer with key '${key}' does not exist`);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
delete reducersMap[key];
|
|
52
52
|
combinedReducer = combineReducers(reducersMap);
|
|
53
53
|
},
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
getReducerMap: () => ({ ...reducersMap }),
|
|
56
56
|
};
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Merges properties from source object into destination object
|
|
61
|
-
* @param {Object} source - Source object
|
|
62
|
-
* @param {Object} destination - Destination object
|
|
63
|
-
* @returns {Object} Merged destination object
|
|
64
61
|
*/
|
|
65
62
|
const mergeObjects = (source, destination) => {
|
|
66
63
|
if (!source || !destination) {
|
|
67
64
|
throw new Error('Both source and destination objects must be provided');
|
|
68
65
|
}
|
|
69
|
-
|
|
66
|
+
|
|
70
67
|
return Object.keys(source).reduce((acc, key) => {
|
|
71
68
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
72
69
|
acc[key] = source[key];
|
|
@@ -75,73 +72,57 @@ export function FeatureFactory(entity, reducers = {}, endpoints = null) {
|
|
|
75
72
|
}, { ...destination });
|
|
76
73
|
};
|
|
77
74
|
|
|
78
|
-
/**
|
|
79
|
-
* Creates standard async thunks for common operations
|
|
80
|
-
* @param {string} sliceName - Name of the slice
|
|
81
|
-
* @returns {Object} Standard async thunks
|
|
82
|
-
*/
|
|
83
|
-
const createStandardThunks = (sliceName) => ({
|
|
84
|
-
fetch: ThunkFactory.create(sliceName, 'all'),
|
|
85
|
-
fetchOne: ThunkFactory.create(sliceName, 'one'),
|
|
86
|
-
search: ThunkFactory.post(sliceName, 'search'),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Adds standard async action handlers to builder
|
|
91
|
-
* @param {Object} builder - RTK builder object
|
|
92
|
-
* @param {Object} asyncActions - Async actions to handle
|
|
93
|
-
* @param {string} sliceName - Name of the slice
|
|
94
|
-
*/
|
|
95
|
-
const addAsyncActionHandlers = (builder, asyncActions, sliceName) => {
|
|
96
|
-
Object.entries(asyncActions).forEach(([key, action]) => {
|
|
97
|
-
builder
|
|
98
|
-
.addCase(action.pending, (state) => {
|
|
99
|
-
state.loading = true;
|
|
100
|
-
state.error = null;
|
|
101
|
-
})
|
|
102
|
-
.addCase(action.fulfilled, (state, actionPayload) => {
|
|
103
|
-
state.loading = false;
|
|
104
|
-
state.success = true;
|
|
105
|
-
state.data = actionPayload.payload;
|
|
106
|
-
|
|
107
|
-
// Handle single item for fetchOne operation
|
|
108
|
-
if (key === 'fetchOne' && Array.isArray(actionPayload.payload)) {
|
|
109
|
-
state[sliceName] = actionPayload.payload[0];
|
|
110
|
-
} else if (Array.isArray(actionPayload.payload) && actionPayload.payload.length > 0) {
|
|
111
|
-
state[sliceName] = actionPayload.payload[0];
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
.addCase(action.rejected, (state, actionPayload) => {
|
|
115
|
-
state.loading = false;
|
|
116
|
-
state.success = false;
|
|
117
|
-
state.error = actionPayload.error || actionPayload.payload;
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
|
|
122
75
|
/**
|
|
123
76
|
* Creates a Redux slice with standard and custom async actions
|
|
124
77
|
* @param {Object} options - Configuration options
|
|
125
78
|
* @param {Object} options.service - Custom service actions
|
|
126
79
|
* @param {Object} options.initialState - Custom initial state
|
|
80
|
+
* @param {Object} options.stateOptions - State factory configuration
|
|
81
|
+
* @param {Object} options.operations - Which standard operations to include
|
|
82
|
+
* @param {boolean} options.includeCommonReducers - Whether to include common reducers
|
|
83
|
+
* @param {Array} options.excludeReducers - List of common reducers to exclude
|
|
127
84
|
* @returns {Object} Created slice and async actions
|
|
128
85
|
*/
|
|
129
86
|
const createFactorySlice = (options = {}) => {
|
|
130
|
-
const {
|
|
87
|
+
const {
|
|
88
|
+
service = null,
|
|
89
|
+
initialState = null,
|
|
90
|
+
stateOptions = {},
|
|
91
|
+
operations = {},
|
|
92
|
+
includeCommonReducers = true,
|
|
93
|
+
excludeReducers = [],
|
|
94
|
+
} = options;
|
|
95
|
+
|
|
131
96
|
const sliceName = entity;
|
|
132
|
-
|
|
133
|
-
// Create standard thunks
|
|
134
|
-
const standardThunks =
|
|
135
|
-
|
|
97
|
+
|
|
98
|
+
// Create standard thunks based on operations config
|
|
99
|
+
const standardThunks = UtilsFactory.createThunks(sliceName, operations);
|
|
100
|
+
|
|
101
|
+
// Filter common reducers based on exclude list
|
|
102
|
+
const filteredCommonReducers = includeCommonReducers
|
|
103
|
+
? Object.entries(commonReducers).reduce((acc, [key, reducer]) => {
|
|
104
|
+
if (!excludeReducers.includes(key)) {
|
|
105
|
+
acc[key] = reducer;
|
|
106
|
+
}
|
|
107
|
+
return acc;
|
|
108
|
+
}, {})
|
|
109
|
+
: {};
|
|
110
|
+
|
|
111
|
+
// Merge all reducers (common + custom)
|
|
112
|
+
const allReducers = {
|
|
113
|
+
...filteredCommonReducers,
|
|
114
|
+
...customReducers,
|
|
115
|
+
};
|
|
116
|
+
|
|
136
117
|
// Create the slice
|
|
137
118
|
const factorySlice = createSlice({
|
|
138
119
|
name: sliceName,
|
|
139
|
-
initialState: initialState || StateFactory(sliceName),
|
|
140
|
-
reducers,
|
|
120
|
+
initialState: initialState || StateFactory(sliceName, stateOptions),
|
|
121
|
+
reducers: allReducers,
|
|
141
122
|
extraReducers: (builder) => {
|
|
142
123
|
// Add standard async action handlers
|
|
143
|
-
|
|
144
|
-
|
|
124
|
+
UtilsFactory.addHandlers(builder, standardThunks, sliceName);
|
|
125
|
+
|
|
145
126
|
// Add custom service action handlers
|
|
146
127
|
if (service && typeof service === 'object') {
|
|
147
128
|
const customActions = Object.entries(service).reduce((acc, [key, action]) => {
|
|
@@ -151,35 +132,100 @@ export function FeatureFactory(entity, reducers = {}, endpoints = null) {
|
|
|
151
132
|
}
|
|
152
133
|
return acc;
|
|
153
134
|
}, {});
|
|
154
|
-
|
|
155
|
-
|
|
135
|
+
|
|
136
|
+
UtilsFactory.addHandlers(builder, customActions, sliceName);
|
|
156
137
|
}
|
|
157
138
|
},
|
|
158
139
|
});
|
|
159
140
|
|
|
160
141
|
// Merge standard and custom async actions
|
|
161
|
-
const asyncActions = service
|
|
142
|
+
const asyncActions = service
|
|
162
143
|
? mergeObjects(standardThunks, service)
|
|
163
144
|
: standardThunks;
|
|
164
145
|
|
|
165
146
|
return {
|
|
166
147
|
slice: factorySlice,
|
|
167
148
|
asyncActions,
|
|
149
|
+
reducers: allReducers,
|
|
168
150
|
};
|
|
169
151
|
};
|
|
170
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Creates a complete CRUD feature with all operations
|
|
155
|
+
*/
|
|
156
|
+
const createCrudFeature = (options = {}) => {
|
|
157
|
+
return createFactorySlice({
|
|
158
|
+
...options,
|
|
159
|
+
operations: {
|
|
160
|
+
fetch: true,
|
|
161
|
+
fetchOne: true,
|
|
162
|
+
search: true,
|
|
163
|
+
create: true,
|
|
164
|
+
update: true,
|
|
165
|
+
patch: true,
|
|
166
|
+
delete: true,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a read-only feature (fetch, fetchOne, search only)
|
|
173
|
+
*/
|
|
174
|
+
const createBasicFeature = (options = {}) => {
|
|
175
|
+
return createFactorySlice({
|
|
176
|
+
...options,
|
|
177
|
+
operations: {
|
|
178
|
+
fetch: true,
|
|
179
|
+
fetchOne: true,
|
|
180
|
+
search: true,
|
|
181
|
+
create: false,
|
|
182
|
+
update: false,
|
|
183
|
+
patch: false,
|
|
184
|
+
delete: false,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
171
189
|
// Public API
|
|
172
190
|
return {
|
|
173
191
|
entity,
|
|
174
|
-
reducers,
|
|
175
192
|
adapter,
|
|
193
|
+
reducers: commonReducers,
|
|
176
194
|
manager: createReducerManager,
|
|
177
|
-
|
|
195
|
+
|
|
196
|
+
// Utilities
|
|
197
|
+
inject: mergeObjects,
|
|
198
|
+
createReducer: (handler) => handler,
|
|
199
|
+
createHandler: (builder, action, handlers) => {
|
|
200
|
+
if (handlers.pending) {
|
|
201
|
+
builder.addCase(action.pending, handlers.pending);
|
|
202
|
+
}
|
|
203
|
+
if (handlers.fulfilled) {
|
|
204
|
+
builder.addCase(action.fulfilled, handlers.fulfilled);
|
|
205
|
+
}
|
|
206
|
+
if (handlers.rejected) {
|
|
207
|
+
builder.addCase(action.rejected, handlers.rejected);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
// Creators
|
|
178
211
|
create: createFactorySlice,
|
|
179
|
-
|
|
212
|
+
createCrud: createCrudFeature,
|
|
213
|
+
createBasic: createBasicFeature,
|
|
214
|
+
createThunks: (operations) => UtilsFactor.createThunks(entity, operations),
|
|
215
|
+
createCustomThunk: (prefix, options) => ThunkFactory.custom(entity, prefix, options),
|
|
216
|
+
|
|
217
|
+
// Getters
|
|
180
218
|
getEntityName: () => entity,
|
|
181
219
|
getAdapter: () => adapter,
|
|
182
|
-
|
|
220
|
+
getCommonReducers: (exclude = []) => {
|
|
221
|
+
return Object.entries(commonReducers).reduce((acc, [key, reducer]) => {
|
|
222
|
+
if (!exclude.includes(key)) {
|
|
223
|
+
acc[key] = reducer;
|
|
224
|
+
}
|
|
225
|
+
return acc;
|
|
226
|
+
}, {});
|
|
227
|
+
},
|
|
228
|
+
}
|
|
183
229
|
}
|
|
184
230
|
|
|
185
231
|
export default FeatureFactory;
|
package/src/factory/thunk.js
CHANGED
|
@@ -17,7 +17,7 @@ const HTTP_METHODS = {
|
|
|
17
17
|
*/
|
|
18
18
|
const STANDARD_OPERATIONS = {
|
|
19
19
|
all: { method: HTTP_METHODS.GET, useParams: false },
|
|
20
|
-
one: { method: HTTP_METHODS.GET, useParams:
|
|
20
|
+
one: { method: HTTP_METHODS.GET, useParams: true, useIdInUrl: true },
|
|
21
21
|
search: { method: HTTP_METHODS.POST, useParams: true },
|
|
22
22
|
new: { method: HTTP_METHODS.POST, useParams: false },
|
|
23
23
|
create: { method: HTTP_METHODS.POST, useParams: false },
|
|
@@ -52,9 +52,9 @@ const validateThunkParams = (entity, prefix) => {
|
|
|
52
52
|
const buildUrl = (entity, prefix, params = {}, useIdInUrl = false) => {
|
|
53
53
|
let url = `${entity}`;
|
|
54
54
|
|
|
55
|
-
if (
|
|
55
|
+
if (params?.id || useIdInUrl) {
|
|
56
56
|
url += `/${params.id}`;
|
|
57
|
-
} else
|
|
57
|
+
} else {
|
|
58
58
|
url += `/${prefix}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { ThunkFactory } from "./thunk";
|
|
2
|
+
import { StateFactory } from "./state";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Common reducer actions that can be dynamically added to any slice
|
|
6
|
+
*/
|
|
7
|
+
export const createReducers = (entity, adapter) => ({
|
|
8
|
+
// Entity management
|
|
9
|
+
setEntity: (state, action) => {
|
|
10
|
+
state[entity] = action.payload;
|
|
11
|
+
state.error = null;
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
clearEntity: (state) => {
|
|
15
|
+
state[entity] = null;
|
|
16
|
+
state.data = [];
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Data management
|
|
20
|
+
setData: (state, action) => {
|
|
21
|
+
state.data = action.payload;
|
|
22
|
+
state.error = null;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
appendData: (state, action) => {
|
|
26
|
+
state.data = [...state.data, ...action.payload];
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
prependData: (state, action) => {
|
|
30
|
+
state.data = [...action.payload, ...state.data];
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
updateDataItem: (state, action) => {
|
|
34
|
+
const { id, updates } = action.payload;
|
|
35
|
+
const index = state.data.findIndex(item => item.id === id);
|
|
36
|
+
if (index !== -1) {
|
|
37
|
+
state.data[index] = { ...state.data[index], ...updates };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
removeDataItem: (state, action) => {
|
|
42
|
+
const id = action.payload;
|
|
43
|
+
state.data = state.data.filter(item => item.id !== id);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Loading state management
|
|
47
|
+
setLoading: (state, action) => {
|
|
48
|
+
state.loading = action.payload;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
setSuccess: (state, action) => {
|
|
52
|
+
state.success = action.payload;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
setError: (state, action) => {
|
|
56
|
+
state.error = action.payload;
|
|
57
|
+
state.loading = false;
|
|
58
|
+
state.success = false;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
clearError: (state) => {
|
|
62
|
+
state.error = null;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Reset operations
|
|
66
|
+
resetState: (state, action) => {
|
|
67
|
+
const namespace = action.payload?.namespace || entity;
|
|
68
|
+
const options = action.payload?.options || {};
|
|
69
|
+
return StateFactory(namespace, options);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
resetData: (state) => {
|
|
73
|
+
state.data = [];
|
|
74
|
+
state[entity] = null;
|
|
75
|
+
state.error = null;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
resetStatus: (state) => {
|
|
79
|
+
state.loading = false;
|
|
80
|
+
state.success = false;
|
|
81
|
+
state.error = null;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Pagination reducers (if enabled)
|
|
85
|
+
setPagination: (state, action) => {
|
|
86
|
+
if (state.pagination) {
|
|
87
|
+
state.pagination = { ...state.pagination, ...action.payload };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
setCurrentPage: (state, action) => {
|
|
92
|
+
if (state.pagination) {
|
|
93
|
+
state.pagination.currentPage = action.payload;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
setPageSize: (state, action) => {
|
|
98
|
+
if (state.pagination) {
|
|
99
|
+
state.pagination.pageSize = action.payload;
|
|
100
|
+
state.pagination.currentPage = 1; // Reset to first page
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Filtering reducers (if enabled)
|
|
105
|
+
setFilters: (state, action) => {
|
|
106
|
+
if (state.filtering) {
|
|
107
|
+
state.filtering.filters = action.payload;
|
|
108
|
+
state.filtering.activeFilters = Object.keys(action.payload).filter(
|
|
109
|
+
key => action.payload[key] !== null && action.payload[key] !== undefined
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
clearFilters: (state) => {
|
|
115
|
+
if (state.filtering) {
|
|
116
|
+
state.filtering.filters = {};
|
|
117
|
+
state.filtering.activeFilters = [];
|
|
118
|
+
state.filtering.searchTerm = '';
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
setSearchTerm: (state, action) => {
|
|
123
|
+
if (state.filtering) {
|
|
124
|
+
state.filtering.searchTerm = action.payload;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Sorting reducers (if enabled)
|
|
129
|
+
setSorting: (state, action) => {
|
|
130
|
+
if (state.sorting) {
|
|
131
|
+
state.sorting.sortBy = action.payload.sortBy;
|
|
132
|
+
state.sorting.sortOrder = action.payload.sortOrder || 'asc';
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
toggleSortOrder: (state) => {
|
|
137
|
+
if (state.sorting) {
|
|
138
|
+
state.sorting.sortOrder = state.sorting.sortOrder === 'asc' ? 'desc' : 'asc';
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Selection reducers (if enabled)
|
|
143
|
+
setSelectedItems: (state, action) => {
|
|
144
|
+
if (state.selection) {
|
|
145
|
+
state.selection.selectedItems = action.payload;
|
|
146
|
+
state.selection.selectedIds = action.payload.map(item => item.id);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
toggleItemSelection: (state, action) => {
|
|
151
|
+
if (state.selection) {
|
|
152
|
+
const id = action.payload;
|
|
153
|
+
const index = state.selection.selectedIds.indexOf(id);
|
|
154
|
+
|
|
155
|
+
if (index > -1) {
|
|
156
|
+
state.selection.selectedIds.splice(index, 1);
|
|
157
|
+
state.selection.selectedItems = state.selection.selectedItems.filter(
|
|
158
|
+
item => item.id !== id
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
const item = state.data.find(item => item.id === id);
|
|
162
|
+
if (item) {
|
|
163
|
+
state.selection.selectedIds.push(id);
|
|
164
|
+
state.selection.selectedItems.push(item);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
state.selection.allSelected =
|
|
169
|
+
state.selection.selectedIds.length === state.data.length && state.data.length > 0;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
selectAll: (state) => {
|
|
174
|
+
if (state.selection) {
|
|
175
|
+
state.selection.selectedItems = [...state.data];
|
|
176
|
+
state.selection.selectedIds = state.data.map(item => item.id);
|
|
177
|
+
state.selection.allSelected = true;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
clearSelection: (state) => {
|
|
182
|
+
if (state.selection) {
|
|
183
|
+
state.selection.selectedItems = [];
|
|
184
|
+
state.selection.selectedIds = [];
|
|
185
|
+
state.selection.allSelected = false;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Metadata reducers (if enabled)
|
|
190
|
+
updateMetadata: (state, action) => {
|
|
191
|
+
if (state.metadata) {
|
|
192
|
+
state.metadata = { ...state.metadata, ...action.payload };
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
markAsStale: (state) => {
|
|
197
|
+
if (state.metadata) {
|
|
198
|
+
state.metadata.stale = true;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
markAsFresh: (state) => {
|
|
203
|
+
if (state.metadata) {
|
|
204
|
+
state.metadata.stale = false;
|
|
205
|
+
state.metadata.lastFetch = new Date().toISOString();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Creates standard async thunks with configurable operations
|
|
212
|
+
*/
|
|
213
|
+
export const createThunks = (entity, operations = {}) => {
|
|
214
|
+
const defaultOperations = {
|
|
215
|
+
fetch: true,
|
|
216
|
+
fetchOne: true,
|
|
217
|
+
search: true,
|
|
218
|
+
create: true,
|
|
219
|
+
update: true,
|
|
220
|
+
patch: true,
|
|
221
|
+
delete: true,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const config = { ...defaultOperations, ...operations };
|
|
225
|
+
const thunks = {};
|
|
226
|
+
|
|
227
|
+
if (config.fetch) {
|
|
228
|
+
thunks.fetch = ThunkFactory.create(entity, 'all');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (config.fetchOne) {
|
|
232
|
+
thunks.fetchOne = ThunkFactory.create(entity, 'one');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (config.search) {
|
|
236
|
+
thunks.search = ThunkFactory.post(entity, 'search');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (config.create) {
|
|
240
|
+
thunks.create = ThunkFactory.post(entity, 'create');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (config.update) {
|
|
244
|
+
thunks.update = ThunkFactory.put(entity, 'update');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (config.patch) {
|
|
248
|
+
thunks.patch = ThunkFactory.patch(entity, 'patch');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (config.delete) {
|
|
252
|
+
thunks.delete = ThunkFactory.delete(entity, 'delete');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return thunks;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Adds standard async action handlers to builder with enhanced logic
|
|
260
|
+
*/
|
|
261
|
+
export const addHandlers = (builder, asyncActions, entity) => {
|
|
262
|
+
Object.entries(asyncActions).forEach(([key, action]) => {
|
|
263
|
+
builder
|
|
264
|
+
.addCase(action.pending, (state) => {
|
|
265
|
+
state.loading = true;
|
|
266
|
+
state.error = null;
|
|
267
|
+
state.loadingState = 'pending';
|
|
268
|
+
|
|
269
|
+
if (state.async?.operations) {
|
|
270
|
+
state.async.operations[key] = {
|
|
271
|
+
loading: true,
|
|
272
|
+
success: false,
|
|
273
|
+
error: null,
|
|
274
|
+
lastRun: new Date().toISOString(),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
.addCase(action.fulfilled, (state, actionPayload) => {
|
|
279
|
+
state.loading = false;
|
|
280
|
+
state.success = true;
|
|
281
|
+
state.loadingState = 'fulfilled';
|
|
282
|
+
|
|
283
|
+
// Handle different response structures
|
|
284
|
+
const payload = actionPayload.payload;
|
|
285
|
+
console.log('payload = ', payload);
|
|
286
|
+
// Update data based on operation type
|
|
287
|
+
if (key === 'fetch' || key === 'search') {
|
|
288
|
+
state.data = Array.isArray(payload) ? payload : (payload?.data.result || []);
|
|
289
|
+
} else if (key === 'fetchOne') {
|
|
290
|
+
state[entity] = Array.isArray(payload) ? payload[0] : payload;
|
|
291
|
+
} else if (key === 'create') {
|
|
292
|
+
if (Array.isArray(state.data)) {
|
|
293
|
+
state.data.push(payload);
|
|
294
|
+
}
|
|
295
|
+
} else if (key === 'update' || key === 'patch') {
|
|
296
|
+
if (Array.isArray(state.data)) {
|
|
297
|
+
const index = state.data.findIndex(item => item.id === payload.id);
|
|
298
|
+
if (index !== -1) {
|
|
299
|
+
state.data[index] = payload;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (state[entity]?.id === payload.id) {
|
|
303
|
+
state[entity] = payload;
|
|
304
|
+
}
|
|
305
|
+
} else if (key === 'delete') {
|
|
306
|
+
if (Array.isArray(state.data)) {
|
|
307
|
+
state.data = state.data.filter(item => item.id !== actionPayload.meta.arg?.id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update metadata
|
|
312
|
+
if (state.metadata) {
|
|
313
|
+
state.metadata.lastUpdate = new Date().toISOString();
|
|
314
|
+
state.metadata.stale = false;
|
|
315
|
+
if (key === 'fetch' || key === 'fetchOne' || key === 'search') {
|
|
316
|
+
state.metadata.lastFetch = new Date().toISOString();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Update async operations tracking
|
|
321
|
+
if (state.async?.operations) {
|
|
322
|
+
state.async.operations[key] = {
|
|
323
|
+
loading: false,
|
|
324
|
+
success: true,
|
|
325
|
+
error: null,
|
|
326
|
+
lastRun: new Date().toISOString(),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
.addCase(action.rejected, (state, actionPayload) => {
|
|
331
|
+
state.loading = false;
|
|
332
|
+
state.success = false;
|
|
333
|
+
state.error = actionPayload.error?.message || actionPayload.payload || 'An error occurred';
|
|
334
|
+
state.loadingState = 'rejected';
|
|
335
|
+
|
|
336
|
+
if (state.async?.operations) {
|
|
337
|
+
state.async.operations[key] = {
|
|
338
|
+
loading: false,
|
|
339
|
+
success: false,
|
|
340
|
+
error: state.error,
|
|
341
|
+
lastRun: new Date().toISOString(),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
export const UtilsFactory = {
|
|
350
|
+
createReducers,
|
|
351
|
+
createThunks,
|
|
352
|
+
addHandlers
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export default UtilsFactory;
|