@smartsoft001/crud-shell-angular 1.2.91 → 2.69.0

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.
Files changed (214) hide show
  1. package/.storybook/addons.js +1 -0
  2. package/.storybook/config.js +5 -0
  3. package/.storybook/tsconfig.json +8 -0
  4. package/.storybook/webpack.config.js +7 -0
  5. package/jest.config.ts +38 -0
  6. package/ng-package.json +7 -0
  7. package/package.json +12 -23
  8. package/project.json +49 -0
  9. package/src/index.ts +13 -0
  10. package/src/lib/+state/crud.facade.ts +82 -0
  11. package/src/lib/+state/crud.feature.methods.ts +258 -0
  12. package/src/lib/+state/crud.feature.store.ts +77 -0
  13. package/src/lib/components/components.module.ts +58 -0
  14. package/src/lib/components/export/export.component.ts +69 -0
  15. package/src/lib/components/filter/base/base.component.ts +175 -0
  16. package/src/lib/components/filter/check/check.component.scss +25 -0
  17. package/src/lib/components/filter/check/check.component.ts +72 -0
  18. package/src/lib/components/filter/date/date.component.html +30 -0
  19. package/src/lib/components/filter/date/date.component.ts +70 -0
  20. package/src/lib/components/filter/date-time/date-time.component.html +62 -0
  21. package/src/lib/components/filter/date-time/date-time.component.ts +56 -0
  22. package/src/lib/components/filter/date-with-edit/date-with-edit.component.html +109 -0
  23. package/src/lib/components/filter/date-with-edit/date-with-edit.component.ts +70 -0
  24. package/src/lib/components/filter/filter.component.html +52 -0
  25. package/src/lib/components/filter/filter.component.ts +34 -0
  26. package/src/lib/components/filter/flag/flag.component.ts +55 -0
  27. package/src/lib/components/filter/index.ts +9 -0
  28. package/src/lib/components/filter/int/int.component.html +74 -0
  29. package/src/lib/components/filter/int/int.component.ts +57 -0
  30. package/src/lib/components/filter/radio/radio.component.ts +74 -0
  31. package/src/lib/components/filter/text/text.component.ts +46 -0
  32. package/src/lib/components/filters/filters.component.scss +150 -0
  33. package/src/lib/components/filters/filters.component.ts +157 -0
  34. package/src/lib/components/filters-config/filters-config.component.ts +69 -0
  35. package/src/lib/components/group/group.component.ts +93 -0
  36. package/src/lib/components/index.ts +7 -0
  37. package/src/lib/components/multiselect/multiselect.component.html +41 -0
  38. package/src/lib/components/multiselect/multiselect.component.scss +125 -0
  39. package/src/lib/components/multiselect/multiselect.component.ts +104 -0
  40. package/src/lib/crud-full.module.ts +51 -0
  41. package/src/lib/crud.config.ts +79 -0
  42. package/src/lib/crud.module.ts +72 -0
  43. package/src/lib/factories/list-pagination/list-pagination.factory.ts +77 -0
  44. package/src/lib/models/index.ts +1 -0
  45. package/src/lib/models/interfaces.ts +34 -0
  46. package/src/lib/pages/index.ts +6 -0
  47. package/src/lib/pages/item/base/base.component.ts +54 -0
  48. package/src/lib/pages/item/item.component.ts +513 -0
  49. package/src/lib/pages/item/standard/standard.component.scss +0 -0
  50. package/src/lib/pages/item/standard/standard.component.ts +50 -0
  51. package/src/lib/pages/list/base/base.component.ts +29 -0
  52. package/src/lib/pages/list/list.component.stories.ts +77 -0
  53. package/src/lib/pages/list/list.component.ts +438 -0
  54. package/src/lib/pages/list/standard/standard.component.ts +42 -0
  55. package/src/lib/pipes/form-options/form-options.pipe.ts +61 -0
  56. package/src/lib/pipes/index.ts +2 -0
  57. package/src/lib/pipes/pipes.module.ts +9 -0
  58. package/src/lib/providers/model-possibilities/model-possibilities.provider.ts +19 -0
  59. package/src/lib/services/crud/crud.service.spec.ts +303 -0
  60. package/src/lib/services/crud/crud.service.ts +178 -0
  61. package/src/lib/services/list-group/list-group.service.spec.ts +87 -0
  62. package/src/lib/services/list-group/list-group.service.ts +65 -0
  63. package/src/lib/services/page/page.service.spec.ts +143 -0
  64. package/src/lib/services/page/page.service.ts +44 -0
  65. package/src/lib/services/search/search.service.spec.ts +134 -0
  66. package/src/lib/services/search/search.service.ts +42 -0
  67. package/test-setup.ts +0 -0
  68. package/tsconfig.json +13 -0
  69. package/tsconfig.lib.json +31 -0
  70. package/tsconfig.lib.prod.json +11 -0
  71. package/tsconfig.spec.json +10 -0
  72. package/tslint.json +10 -0
  73. package/esm2022/index.mjs +0 -18
  74. package/esm2022/lib/+state/crud.actions.mjs +0 -224
  75. package/esm2022/lib/+state/crud.effects.mjs +0 -182
  76. package/esm2022/lib/+state/crud.facade.mjs +0 -91
  77. package/esm2022/lib/+state/crud.reducer.mjs +0 -214
  78. package/esm2022/lib/+state/crud.selectors.mjs +0 -11
  79. package/esm2022/lib/components/components.module.mjs +0 -110
  80. package/esm2022/lib/components/export/export.component.mjs +0 -47
  81. package/esm2022/lib/components/filter/base/base.component.mjs +0 -152
  82. package/esm2022/lib/components/filter/check/check.component.mjs +0 -36
  83. package/esm2022/lib/components/filter/date/date.component.mjs +0 -47
  84. package/esm2022/lib/components/filter/date-time/date-time.component.mjs +0 -43
  85. package/esm2022/lib/components/filter/date-with-edit/date-with-edit.component.mjs +0 -49
  86. package/esm2022/lib/components/filter/filter.component.mjs +0 -30
  87. package/esm2022/lib/components/filter/flag/flag.component.mjs +0 -16
  88. package/esm2022/lib/components/filter/index.mjs +0 -10
  89. package/esm2022/lib/components/filter/int/int.component.mjs +0 -38
  90. package/esm2022/lib/components/filter/radio/radio.component.mjs +0 -16
  91. package/esm2022/lib/components/filter/text/text.component.mjs +0 -16
  92. package/esm2022/lib/components/filters/filters.component.mjs +0 -111
  93. package/esm2022/lib/components/filters-config/filters-config.component.mjs +0 -37
  94. package/esm2022/lib/components/group/group.component.mjs +0 -54
  95. package/esm2022/lib/components/index.mjs +0 -8
  96. package/esm2022/lib/components/multiselect/multiselect.component.mjs +0 -71
  97. package/esm2022/lib/crud-full.module.mjs +0 -103
  98. package/esm2022/lib/crud.config.mjs +0 -17
  99. package/esm2022/lib/crud.module.mjs +0 -106
  100. package/esm2022/lib/factories/list-pagination/list-pagination.factory.mjs +0 -69
  101. package/esm2022/lib/models/index.mjs +0 -2
  102. package/esm2022/lib/models/interfaces.mjs +0 -2
  103. package/esm2022/lib/pages/index.mjs +0 -7
  104. package/esm2022/lib/pages/item/base/base.component.mjs +0 -44
  105. package/esm2022/lib/pages/item/item.component.mjs +0 -395
  106. package/esm2022/lib/pages/item/standard/standard.component.mjs +0 -15
  107. package/esm2022/lib/pages/list/base/base.component.mjs +0 -23
  108. package/esm2022/lib/pages/list/list.component.mjs +0 -355
  109. package/esm2022/lib/pages/list/standard/standard.component.mjs +0 -34
  110. package/esm2022/lib/pipes/form-options/form-options.pipe.mjs +0 -55
  111. package/esm2022/lib/pipes/index.mjs +0 -3
  112. package/esm2022/lib/pipes/pipes.module.mjs +0 -20
  113. package/esm2022/lib/providers/model-possibilities/model-possibilities.provider.mjs +0 -11
  114. package/esm2022/lib/services/crud/crud.service.mjs +0 -138
  115. package/esm2022/lib/services/list-group/list-group.service.mjs +0 -60
  116. package/esm2022/lib/services/page/page.service.mjs +0 -41
  117. package/esm2022/lib/services/search/search.service.mjs +0 -41
  118. package/esm2022/lib/services/socket/socket.service.mjs +0 -17
  119. package/esm2022/smartsoft001-crud-shell-angular.mjs +0 -5
  120. package/fesm2022/smartsoft001-crud-shell-angular.mjs +0 -2810
  121. package/fesm2022/smartsoft001-crud-shell-angular.mjs.map +0 -1
  122. package/index.d.ts +0 -18
  123. package/index.d.ts.map +0 -1
  124. package/lib/+state/crud.actions.d.ts +0 -152
  125. package/lib/+state/crud.actions.d.ts.map +0 -1
  126. package/lib/+state/crud.effects.d.ts +0 -18
  127. package/lib/+state/crud.effects.d.ts.map +0 -1
  128. package/lib/+state/crud.facade.d.ts +0 -44
  129. package/lib/+state/crud.facade.d.ts.map +0 -1
  130. package/lib/+state/crud.reducer.d.ts +0 -43
  131. package/lib/+state/crud.reducer.d.ts.map +0 -1
  132. package/lib/+state/crud.selectors.d.ts +0 -10
  133. package/lib/+state/crud.selectors.d.ts.map +0 -1
  134. package/lib/components/components.module.d.ts +0 -27
  135. package/lib/components/components.module.d.ts.map +0 -1
  136. package/lib/components/export/export.component.d.ts +0 -21
  137. package/lib/components/export/export.component.d.ts.map +0 -1
  138. package/lib/components/filter/base/base.component.d.ts +0 -39
  139. package/lib/components/filter/base/base.component.d.ts.map +0 -1
  140. package/lib/components/filter/check/check.component.d.ts +0 -19
  141. package/lib/components/filter/check/check.component.d.ts.map +0 -1
  142. package/lib/components/filter/date/date.component.d.ts +0 -16
  143. package/lib/components/filter/date/date.component.d.ts.map +0 -1
  144. package/lib/components/filter/date-time/date-time.component.d.ts +0 -15
  145. package/lib/components/filter/date-time/date-time.component.d.ts.map +0 -1
  146. package/lib/components/filter/date-with-edit/date-with-edit.component.d.ts +0 -17
  147. package/lib/components/filter/date-with-edit/date-with-edit.component.d.ts.map +0 -1
  148. package/lib/components/filter/filter.component.d.ts +0 -14
  149. package/lib/components/filter/filter.component.d.ts.map +0 -1
  150. package/lib/components/filter/flag/flag.component.d.ts +0 -8
  151. package/lib/components/filter/flag/flag.component.d.ts.map +0 -1
  152. package/lib/components/filter/index.d.ts +0 -10
  153. package/lib/components/filter/index.d.ts.map +0 -1
  154. package/lib/components/filter/int/int.component.d.ts +0 -15
  155. package/lib/components/filter/int/int.component.d.ts.map +0 -1
  156. package/lib/components/filter/radio/radio.component.d.ts +0 -8
  157. package/lib/components/filter/radio/radio.component.d.ts.map +0 -1
  158. package/lib/components/filter/text/text.component.d.ts +0 -8
  159. package/lib/components/filter/text/text.component.d.ts.map +0 -1
  160. package/lib/components/filters/filters.component.d.ts +0 -83
  161. package/lib/components/filters/filters.component.d.ts.map +0 -1
  162. package/lib/components/filters-config/filters-config.component.d.ts +0 -19
  163. package/lib/components/filters-config/filters-config.component.d.ts.map +0 -1
  164. package/lib/components/group/group.component.d.ts +0 -23
  165. package/lib/components/group/group.component.d.ts.map +0 -1
  166. package/lib/components/index.d.ts +0 -8
  167. package/lib/components/index.d.ts.map +0 -1
  168. package/lib/components/multiselect/multiselect.component.d.ts +0 -28
  169. package/lib/components/multiselect/multiselect.component.d.ts.map +0 -1
  170. package/lib/crud-full.module.d.ts +0 -28
  171. package/lib/crud-full.module.d.ts.map +0 -1
  172. package/lib/crud.config.d.ts +0 -65
  173. package/lib/crud.config.d.ts.map +0 -1
  174. package/lib/crud.module.d.ts +0 -34
  175. package/lib/crud.module.d.ts.map +0 -1
  176. package/lib/factories/list-pagination/list-pagination.factory.d.ts +0 -20
  177. package/lib/factories/list-pagination/list-pagination.factory.d.ts.map +0 -1
  178. package/lib/models/index.d.ts +0 -2
  179. package/lib/models/index.d.ts.map +0 -1
  180. package/lib/models/interfaces.d.ts +0 -30
  181. package/lib/models/interfaces.d.ts.map +0 -1
  182. package/lib/pages/index.d.ts +0 -7
  183. package/lib/pages/index.d.ts.map +0 -1
  184. package/lib/pages/item/base/base.component.d.ts +0 -25
  185. package/lib/pages/item/base/base.component.d.ts.map +0 -1
  186. package/lib/pages/item/item.component.d.ts +0 -70
  187. package/lib/pages/item/item.component.d.ts.map +0 -1
  188. package/lib/pages/item/standard/standard.component.d.ts +0 -8
  189. package/lib/pages/item/standard/standard.component.d.ts.map +0 -1
  190. package/lib/pages/list/base/base.component.d.ts +0 -15
  191. package/lib/pages/list/base/base.component.d.ts.map +0 -1
  192. package/lib/pages/list/list.component.d.ts +0 -54
  193. package/lib/pages/list/list.component.d.ts.map +0 -1
  194. package/lib/pages/list/standard/standard.component.d.ts +0 -14
  195. package/lib/pages/list/standard/standard.component.d.ts.map +0 -1
  196. package/lib/pipes/form-options/form-options.pipe.d.ts +0 -15
  197. package/lib/pipes/form-options/form-options.pipe.d.ts.map +0 -1
  198. package/lib/pipes/index.d.ts +0 -3
  199. package/lib/pipes/index.d.ts.map +0 -1
  200. package/lib/pipes/pipes.module.d.ts +0 -8
  201. package/lib/pipes/pipes.module.d.ts.map +0 -1
  202. package/lib/providers/model-possibilities/model-possibilities.provider.d.ts +0 -18
  203. package/lib/providers/model-possibilities/model-possibilities.provider.d.ts.map +0 -1
  204. package/lib/services/crud/crud.service.d.ts +0 -38
  205. package/lib/services/crud/crud.service.d.ts.map +0 -1
  206. package/lib/services/list-group/list-group.service.d.ts +0 -15
  207. package/lib/services/list-group/list-group.service.d.ts.map +0 -1
  208. package/lib/services/page/page.service.d.ts +0 -12
  209. package/lib/services/page/page.service.d.ts.map +0 -1
  210. package/lib/services/search/search.service.d.ts +0 -16
  211. package/lib/services/search/search.service.d.ts.map +0 -1
  212. package/lib/services/socket/socket.service.d.ts +0 -10
  213. package/lib/services/socket/socket.service.d.ts.map +0 -1
  214. package/smartsoft001-crud-shell-angular.d.ts.map +0 -1
@@ -0,0 +1,303 @@
1
+ import '@angular/compiler';
2
+ import 'zone.js';
3
+ import 'zone.js/testing';
4
+ import { HttpClient } from '@angular/common/http';
5
+ import {
6
+ HttpTestingController,
7
+ HttpClientTestingModule,
8
+ } from '@angular/common/http/testing';
9
+ import { TestBed } from '@angular/core/testing';
10
+ import {
11
+ BrowserDynamicTestingModule,
12
+ platformBrowserDynamicTesting,
13
+ } from '@angular/platform-browser-dynamic/testing';
14
+
15
+ import { IEntity } from '@smartsoft001/domain-core';
16
+
17
+ import { CrudService } from './crud.service';
18
+ import { CrudConfig } from '../../crud.config';
19
+ import { ICrudCreateManyOptions, ICrudFilter } from '../../models';
20
+
21
+ interface TestEntity extends IEntity<string> {
22
+ id: string;
23
+ name: string;
24
+ }
25
+
26
+ describe('crud-shell-angular: CrudService', () => {
27
+ let service: CrudService<TestEntity>;
28
+ let httpMock: HttpTestingController;
29
+ let httpClient: HttpClient;
30
+ let config: CrudConfig<TestEntity>;
31
+
32
+ // Mock URL.createObjectURL globally before tests
33
+ const mockCreateObjectURL = jest.fn().mockReturnValue('blob-url');
34
+ global.URL.createObjectURL = mockCreateObjectURL;
35
+
36
+ TestBed.initTestEnvironment(
37
+ BrowserDynamicTestingModule,
38
+ platformBrowserDynamicTesting(),
39
+ {
40
+ teardown: { destroyAfterEach: true },
41
+ },
42
+ );
43
+
44
+ beforeEach(() => {
45
+ config = new CrudConfig<TestEntity>();
46
+ config.entity = '';
47
+ config.apiUrl = 'http://api.test/entities';
48
+
49
+ TestBed.configureTestingModule({
50
+ imports: [HttpClientTestingModule],
51
+ providers: [
52
+ {
53
+ provide: CrudConfig,
54
+ useValue: config,
55
+ },
56
+ {
57
+ provide: CrudService,
58
+ useFactory: () => new CrudService<TestEntity>(),
59
+ },
60
+ ],
61
+ });
62
+
63
+ service = TestBed.inject(CrudService);
64
+ httpMock = TestBed.inject(HttpTestingController);
65
+ });
66
+
67
+ afterEach(() => {
68
+ httpMock.verify();
69
+ });
70
+
71
+ describe('create', () => {
72
+ it('should create an entity and return id from location header', async () => {
73
+ const testEntity: TestEntity = { id: '', name: 'Test Entity' };
74
+
75
+ service.create(testEntity);
76
+
77
+ const req = httpMock.expectOne(config.apiUrl);
78
+ expect(req.request.method).toBe('POST');
79
+ expect(req.request.body).toEqual(testEntity);
80
+ });
81
+
82
+ it('should return null when location header is missing', async () => {
83
+ const testEntity: TestEntity = { id: '', name: 'Test Entity' };
84
+
85
+ const promise = service.create(testEntity);
86
+
87
+ const req = httpMock.expectOne(config.apiUrl);
88
+ req.flush(null, { headers: {} });
89
+
90
+ const result = await promise;
91
+ expect(result).toBeNull();
92
+ });
93
+ });
94
+
95
+ describe('createMany', () => {
96
+ it('should create multiple entities', async () => {
97
+ const testEntities: TestEntity[] = [
98
+ { id: '', name: 'Entity 1' },
99
+ { id: '', name: 'Entity 2' },
100
+ ];
101
+ const options: ICrudCreateManyOptions = { mode: 'default' };
102
+
103
+ const promise = service.createMany(testEntities, options);
104
+
105
+ const req = httpMock.expectOne(`${config.apiUrl}/bulk?mode=default`);
106
+ expect(req.request.method).toBe('POST');
107
+ expect(req.request.body).toEqual(testEntities);
108
+
109
+ // req.flush(null);
110
+ //
111
+ // await promise;
112
+ });
113
+ });
114
+
115
+ describe('getById', () => {
116
+ it('should fetch entity by id', async () => {
117
+ const testId = '123';
118
+ const expectedEntity: TestEntity = { id: testId, name: 'Test Entity' };
119
+
120
+ const promise = service.getById(testId);
121
+
122
+ const req = httpMock.expectOne(`${config.apiUrl}/${testId}`);
123
+ expect(req.request.method).toBe('GET');
124
+
125
+ req.flush(expectedEntity);
126
+
127
+ const result = await promise;
128
+ expect(result).toEqual(expectedEntity);
129
+ });
130
+ });
131
+
132
+ describe('getList', () => {
133
+ it('should fetch list of entities without filter', async () => {
134
+ const expectedResponse = {
135
+ data: [{ id: '1', name: 'Entity 1' }],
136
+ totalCount: 1,
137
+ links: {},
138
+ };
139
+
140
+ const promise = service.getList();
141
+
142
+ const req = httpMock.expectOne(config.apiUrl);
143
+ expect(req.request.method).toBe('GET');
144
+
145
+ req.flush(expectedResponse);
146
+
147
+ const result = await promise;
148
+ expect(result).toEqual(expectedResponse);
149
+ });
150
+
151
+ it('should fetch list of entities with search filter', async () => {
152
+ const filter: ICrudFilter = { searchText: 'test' };
153
+ const expectedResponse = {
154
+ data: [{ id: '1', name: 'Test Entity' }],
155
+ totalCount: 1,
156
+ links: {},
157
+ };
158
+
159
+ const promise = service.getList(filter);
160
+
161
+ const req = httpMock.expectOne(`${config.apiUrl}?$search=test`);
162
+ expect(req.request.method).toBe('GET');
163
+
164
+ req.flush(expectedResponse);
165
+
166
+ const result = await promise;
167
+ expect(result).toEqual(expectedResponse);
168
+ });
169
+ });
170
+
171
+ describe('update', () => {
172
+ it('should update an entity', async () => {
173
+ const testEntity: TestEntity = { id: '123', name: 'Updated Entity' };
174
+
175
+ const promise = service.update(testEntity);
176
+
177
+ const req = httpMock.expectOne(`${config.apiUrl}/${testEntity.id}`);
178
+ expect(req.request.method).toBe('PUT');
179
+ expect(req.request.body).toEqual(testEntity);
180
+
181
+ req.flush(null);
182
+
183
+ await promise;
184
+ });
185
+ });
186
+
187
+ describe('updatePartial', () => {
188
+ it('should partially update an entity', async () => {
189
+ const partialUpdate = { id: '123', name: 'Partially Updated' };
190
+
191
+ const promise = service.updatePartial(partialUpdate);
192
+
193
+ const req = httpMock.expectOne(`${config.apiUrl}/${partialUpdate.id}`);
194
+ expect(req.request.method).toBe('PATCH');
195
+ expect(req.request.body).toEqual(partialUpdate);
196
+
197
+ req.flush(null);
198
+
199
+ await promise;
200
+ });
201
+ });
202
+
203
+ describe('updatePartialMany', () => {
204
+ it('should update multiple entities partially', async () => {
205
+ const updates = [
206
+ { id: '1', name: 'Update 1' },
207
+ { id: '2', name: 'Update 2' },
208
+ ];
209
+
210
+ const promise = service.updatePartialMany(updates);
211
+
212
+ updates.forEach((update) => {
213
+ const req = httpMock.expectOne(`${config.apiUrl}/${update.id}`);
214
+ expect(req.request.method).toBe('PATCH');
215
+ expect(req.request.body).toEqual(update);
216
+ req.flush(null);
217
+ });
218
+
219
+ await promise;
220
+ });
221
+ });
222
+
223
+ describe('delete', () => {
224
+ it('should delete an entity', async () => {
225
+ const testId = '123';
226
+
227
+ const promise = service.delete(testId);
228
+
229
+ const req = httpMock.expectOne(`${config.apiUrl}/${testId}`);
230
+ expect(req.request.method).toBe('DELETE');
231
+
232
+ req.flush(null);
233
+
234
+ await promise;
235
+ });
236
+ });
237
+
238
+ describe('exportList', () => {
239
+ beforeEach(() => {
240
+ // Reset the mock before each test
241
+ mockCreateObjectURL.mockClear();
242
+ });
243
+
244
+ it('should export list as CSV', async () => {
245
+ const format = 'csv';
246
+ const csvData = 'id,name\n1,Test';
247
+
248
+ // Mock document.createElement
249
+ const mockLink = {
250
+ href: '',
251
+ download: '',
252
+ click: jest.fn(),
253
+ };
254
+ jest.spyOn(document, 'createElement').mockReturnValue(mockLink as any);
255
+ jest.spyOn(document.body, 'appendChild').mockImplementation();
256
+ jest.spyOn(document.body, 'removeChild').mockImplementation();
257
+
258
+ const promise = service.exportList(null, format);
259
+
260
+ const req = httpMock.expectOne(config.apiUrl);
261
+ expect(req.request.method).toBe('GET');
262
+ expect(req.request.headers.get('Content-Type')).toBe('text/csv');
263
+
264
+ req.flush(csvData);
265
+
266
+ await promise;
267
+
268
+ expect(mockCreateObjectURL).toHaveBeenCalled();
269
+ expect(mockLink.download).toBe('data.csv');
270
+ expect(mockLink.click).toHaveBeenCalled();
271
+ });
272
+
273
+ it('should export list as XLSX', async () => {
274
+ const format = 'xlsx';
275
+ const xlsxData = new Blob(['fake xlsx data']);
276
+
277
+ const mockLink = {
278
+ href: '',
279
+ download: '',
280
+ click: jest.fn(),
281
+ };
282
+ jest.spyOn(document, 'createElement').mockReturnValue(mockLink as any);
283
+ jest.spyOn(document.body, 'appendChild').mockImplementation();
284
+ jest.spyOn(document.body, 'removeChild').mockImplementation();
285
+
286
+ const promise = service.exportList(null, format);
287
+
288
+ const req = httpMock.expectOne(config.apiUrl);
289
+ expect(req.request.method).toBe('GET');
290
+ expect(req.request.headers.get('Content-Type')).toBe(
291
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
292
+ );
293
+
294
+ req.flush(xlsxData);
295
+
296
+ await promise;
297
+
298
+ expect(mockCreateObjectURL).toHaveBeenCalled();
299
+ expect(mockLink.download).toBe('data.xlsx');
300
+ expect(mockLink.click).toHaveBeenCalled();
301
+ });
302
+ });
303
+ });
@@ -0,0 +1,178 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import { inject, Injectable } from '@angular/core';
3
+ import { firstValueFrom } from 'rxjs';
4
+ import { map } from 'rxjs/operators';
5
+
6
+ import { IEntity } from '@smartsoft001/domain-core';
7
+
8
+ import { CrudConfig } from '../../crud.config';
9
+ import { ICrudCreateManyOptions, ICrudFilter } from '../../models';
10
+
11
+ @Injectable()
12
+ export class CrudService<T extends IEntity<string>> {
13
+ protected config = inject(CrudConfig<T>);
14
+ protected http = inject(HttpClient);
15
+
16
+ protected _formatMap = {
17
+ csv: 'text/csv',
18
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
19
+ };
20
+
21
+ create(item: T): Promise<string> {
22
+ return firstValueFrom(
23
+ this.http
24
+ .post<void>(this.config.apiUrl, item, { observe: 'response' })
25
+ .pipe(
26
+ map((response) => {
27
+ const location = response.headers['Location'];
28
+ if (!location) return null;
29
+ const array = location.split('/');
30
+ return array[array.length - 1];
31
+ }),
32
+ ),
33
+ );
34
+ }
35
+
36
+ createMany(items: Array<T>, options: ICrudCreateManyOptions): Promise<void> {
37
+ return firstValueFrom(
38
+ this.http.post<void>(
39
+ this.config.apiUrl + '/bulk?mode=' + options.mode,
40
+ items,
41
+ ),
42
+ );
43
+ }
44
+
45
+ getById(id: string): Promise<T> {
46
+ return firstValueFrom(this.http.get<T>(this.config.apiUrl + '/' + id));
47
+ }
48
+
49
+ getList<T>(
50
+ filter: ICrudFilter = null,
51
+ ): Promise<{ data: T[]; totalCount: number; links }> {
52
+ return firstValueFrom(
53
+ this.http.get<{ data: T[]; totalCount: number; links }>(
54
+ this.config.apiUrl + this.getQuery(filter),
55
+ ),
56
+ );
57
+ }
58
+
59
+ exportList(
60
+ filter: ICrudFilter = null,
61
+ format: 'csv' | 'xlsx',
62
+ ): Promise<void> {
63
+ if (!format) {
64
+ format = 'csv';
65
+ }
66
+
67
+ if (format === 'xlsx') {
68
+ return firstValueFrom(
69
+ this.http
70
+ .get(this.config.apiUrl + this.getQuery(filter), {
71
+ headers: {
72
+ 'Content-Type': this._formatMap[format],
73
+ },
74
+ observe: 'response',
75
+ reportProgress: false,
76
+ responseType: 'blob',
77
+ })
78
+ .pipe(
79
+ map((res) => {
80
+ const downloadLink = document.createElement('a');
81
+ const blob = res.body;
82
+
83
+ downloadLink.href = URL.createObjectURL(blob);
84
+ downloadLink.download = 'data.' + format;
85
+
86
+ document.body.appendChild(downloadLink);
87
+ downloadLink.click();
88
+ document.body.removeChild(downloadLink);
89
+ }),
90
+ ),
91
+ );
92
+ }
93
+
94
+ return firstValueFrom(
95
+ this.http
96
+ .get<string>(this.config.apiUrl + this.getQuery(filter), {
97
+ headers: {
98
+ 'Content-Type': this._formatMap[format],
99
+ },
100
+ responseType: 'text' as 'json',
101
+ })
102
+ .pipe(
103
+ map((res) => {
104
+ const downloadLink = document.createElement('a');
105
+ const blob = new Blob(['\ufeff', res]);
106
+
107
+ downloadLink.href = URL.createObjectURL(blob);
108
+ downloadLink.download = 'data.' + format;
109
+
110
+ document.body.appendChild(downloadLink);
111
+ downloadLink.click();
112
+ document.body.removeChild(downloadLink);
113
+ }),
114
+ ),
115
+ );
116
+ }
117
+
118
+ update(item: T): Promise<void> {
119
+ return firstValueFrom(
120
+ this.http.put<void>(this.config.apiUrl + '/' + item.id, item),
121
+ );
122
+ }
123
+
124
+ updatePartial(item: Partial<T> & { id: string }): Promise<void> {
125
+ return firstValueFrom(
126
+ this.http.patch<void>(this.config.apiUrl + '/' + item.id, item),
127
+ );
128
+ }
129
+
130
+ updatePartialMany(items: (Partial<T> & { id: string })[]): Promise<any> {
131
+ const updatePromises = items.map((item) => {
132
+ return this.updatePartial(item);
133
+ });
134
+ return Promise.all(updatePromises);
135
+ }
136
+
137
+ delete(id: string): Promise<void> {
138
+ return firstValueFrom(
139
+ this.http.delete<void>(this.config.apiUrl + '/' + id),
140
+ );
141
+ }
142
+
143
+ protected getQuery(filter: ICrudFilter): string {
144
+ let query = '';
145
+
146
+ if (filter && filter.searchText) {
147
+ query += '&$search=' + filter.searchText;
148
+ }
149
+
150
+ if (filter && filter.limit) {
151
+ query += `&limit=${filter.limit}&offset=${
152
+ filter.offset ? filter.offset : 0
153
+ }`;
154
+ }
155
+
156
+ if (filter && filter.sortBy) {
157
+ query += `&sort=${(filter.sortDesc ? '-' : '') + filter.sortBy}`;
158
+ }
159
+
160
+ if (filter && filter.query) {
161
+ filter.query.forEach((q) => {
162
+ if (
163
+ q.value &&
164
+ typeof q.value === 'string' &&
165
+ q.value.match(/^-?\d+$/) &&
166
+ q.value[0] !== "'" &&
167
+ q.value[0] !== '"'
168
+ ) {
169
+ query += '&' + q.key + q.type + `'${q.value}'`;
170
+ } else {
171
+ query += '&' + q.key + q.type + q.value;
172
+ }
173
+ });
174
+ }
175
+
176
+ return query ? '?' + query.replace('&', '') : '';
177
+ }
178
+ }
@@ -0,0 +1,87 @@
1
+ // Mock the facade before any imports
2
+ import { ɵNoopNgZone as NoopNgZone, NgZone } from '@angular/core';
3
+ import { TestBed } from '@angular/core/testing';
4
+ import {
5
+ BrowserTestingModule,
6
+ platformBrowserTesting,
7
+ } from '@angular/platform-browser/testing';
8
+
9
+ import { IEntity } from '@smartsoft001/domain-core';
10
+
11
+ import { CrudListGroupService } from './list-group.service';
12
+ import { CrudFacade } from '../../+state/crud.facade';
13
+ import { ICrudFilter, ICrudListGroup } from '../../models';
14
+
15
+ jest.mock('../../+state/crud.facade', () => ({
16
+ CrudFacade: jest.fn().mockImplementation(() => {
17
+ const filter: ICrudFilter = {
18
+ query: [],
19
+ };
20
+ return {
21
+ filter: jest.fn(() => filter),
22
+ read: jest.fn(() => []),
23
+ };
24
+ }),
25
+ }));
26
+
27
+ export class MockCrudFacade<T> {
28
+ filter: jest.Mock;
29
+ read: jest.Mock;
30
+
31
+ constructor() {
32
+ const filter: ICrudFilter = { query: [] };
33
+ this.filter = jest.fn(() => filter);
34
+ this.read = jest.fn(() => []);
35
+ }
36
+ }
37
+
38
+ TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
39
+
40
+ describe('crud-shell-angular: CrudListGroupService', () => {
41
+ let service: CrudListGroupService<IEntity<string>>;
42
+ let facade: CrudFacade<IEntity<string>>;
43
+
44
+ beforeEach(() => {
45
+ TestBed.configureTestingModule({
46
+ providers: [
47
+ CrudListGroupService,
48
+ { provide: CrudFacade, useClass: MockCrudFacade },
49
+ { provide: NgZone, useClass: NoopNgZone },
50
+ ],
51
+ });
52
+ service = TestBed.inject(CrudListGroupService);
53
+ facade = TestBed.inject(CrudFacade);
54
+ });
55
+
56
+ it('should create the service', () => {
57
+ expect(service).toBeTruthy();
58
+ });
59
+
60
+ describe('change', () => {
61
+ it('should set item.changed to true if val is truthy', () => {
62
+ const item: ICrudListGroup = { key: 'test', value: 'val' } as any;
63
+ service.change(true, item, false);
64
+ expect(item.changed).toBe(true);
65
+ });
66
+
67
+ it('should set item.changed to true if force is true', () => {
68
+ const item: ICrudListGroup = { key: 'test', value: 'val' } as any;
69
+ service.change(false, item, true);
70
+ expect(item.changed).toBe(true);
71
+ });
72
+
73
+ it('should add a new query if not found', () => {
74
+ const item: ICrudListGroup = { key: 'test', value: 'val' } as any;
75
+ service.change(true, item, false);
76
+ expect(facade.filter().query.length).toBe(1);
77
+ expect(facade.filter().query[0].key).toBe('test');
78
+ });
79
+
80
+ it('should update current.value and set current.hidden to true', () => {
81
+ const item: ICrudListGroup = { key: 'test', value: 'val' } as any;
82
+ service.change(true, item, false);
83
+ expect(facade.filter().query[0].value).toBe('val');
84
+ expect(facade.filter().query[0].hidden).toBe(true);
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,65 @@
1
+ import { inject, Injectable } from '@angular/core';
2
+ import { debounce } from 'lodash-decorators';
3
+
4
+ import { IEntity } from '@smartsoft001/domain-core';
5
+
6
+ import { CrudFacade } from '../../+state/crud.facade';
7
+ import { ICrudListGroup } from '../../models';
8
+
9
+ @Injectable()
10
+ export class CrudListGroupService<T extends IEntity<string>> {
11
+ private facade = inject(CrudFacade<T>);
12
+
13
+ private _destroyed: Array<ICrudListGroup> = [];
14
+
15
+ change(val: boolean, item: ICrudListGroup, force = false): void {
16
+ if (val || force) {
17
+ item.changed = true;
18
+
19
+ let current = this.facade
20
+ .filter()
21
+ .query.find((q) => q.key === item.key && q.type === '=');
22
+
23
+ if (!current) {
24
+ current = {
25
+ key: item.key,
26
+ type: '=',
27
+ value: null,
28
+ };
29
+ this.facade.filter().query.push(current);
30
+ }
31
+
32
+ current.value = item.value;
33
+ current.hidden = true;
34
+
35
+ this.facade.read({
36
+ ...this.facade.filter(),
37
+ offset: 0,
38
+ });
39
+ }
40
+ }
41
+
42
+ destroy(groups: Array<ICrudListGroup>): void {
43
+ this._destroyed = [
44
+ ...(this._destroyed ? this._destroyed : []),
45
+ ...groups.filter((g) => g.changed),
46
+ ];
47
+
48
+ this.refresh();
49
+ }
50
+
51
+ @debounce(250)
52
+ private refresh(): void {
53
+ const list = this._destroyed;
54
+ this._destroyed = [];
55
+
56
+ const newQuery = this.facade
57
+ .filter()
58
+ .query.filter((q) => !list.some((i) => q.key === i.key));
59
+
60
+ this.facade.read({
61
+ ...this.facade.filter(),
62
+ query: newQuery,
63
+ });
64
+ }
65
+ }