bt-core-app 1.4.2 → 1.4.3

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 (68) hide show
  1. package/package.json +8 -2
  2. package/dist/useApi.d.ts +0 -0
  3. package/src/assets/vue.svg +0 -1
  4. package/src/components/BT-Btn.vue +0 -41
  5. package/src/components/BT-Col.vue +0 -36
  6. package/src/components/BT-Field-Checkbox.vue +0 -70
  7. package/src/components/BT-Field-Date.vue +0 -108
  8. package/src/components/BT-Field-Entity.vue +0 -48
  9. package/src/components/BT-Field-Select.vue +0 -67
  10. package/src/components/BT-Field-String.vue +0 -78
  11. package/src/components/BT-Field-Switch.vue +0 -68
  12. package/src/components/BT-Field-Tags.vue +0 -66
  13. package/src/components/BT-Field-Textarea.vue +0 -67
  14. package/src/components/BT-Field-Trigger.vue +0 -315
  15. package/src/components/BT-Header-Option.vue +0 -39
  16. package/src/components/BT-Json.vue +0 -67
  17. package/src/components/BT-Nav-Menu-Item.vue +0 -60
  18. package/src/components/BT-Nav-Sidebar.vue +0 -78
  19. package/src/components/BT-Select-List-Box.vue +0 -258
  20. package/src/components/BT-Select.vue +0 -46
  21. package/src/components/BT-Snack.vue +0 -31
  22. package/src/components/BT-Span.vue +0 -21
  23. package/src/components/Dialog-Confirm.vue +0 -47
  24. package/src/components/Dialog-Select-Date.vue +0 -89
  25. package/src/components/Dialog-Select.vue +0 -140
  26. package/src/components/Dialog-Text.vue +0 -59
  27. package/src/composables/actions-tracker.ts +0 -99
  28. package/src/composables/actions.ts +0 -354
  29. package/src/composables/api.ts +0 -479
  30. package/src/composables/auth.ts +0 -452
  31. package/src/composables/cosmetics.ts +0 -177
  32. package/src/composables/csv.ts +0 -198
  33. package/src/composables/dates.ts +0 -86
  34. package/src/composables/demo.ts +0 -33
  35. package/src/composables/dialogs.ts +0 -114
  36. package/src/composables/document-meta.ts +0 -44
  37. package/src/composables/draggable.ts +0 -189
  38. package/src/composables/filters.ts +0 -264
  39. package/src/composables/forage.ts +0 -49
  40. package/src/composables/helpers.ts +0 -647
  41. package/src/composables/id.ts +0 -20
  42. package/src/composables/list.ts +0 -604
  43. package/src/composables/navigation.ts +0 -222
  44. package/src/composables/presets.ts +0 -28
  45. package/src/composables/pwa.ts +0 -97
  46. package/src/composables/resizable.ts +0 -382
  47. package/src/composables/rules.ts +0 -40
  48. package/src/composables/stores.ts +0 -818
  49. package/src/composables/track.ts +0 -55
  50. package/src/composables/urls.ts +0 -66
  51. package/src/core.ts +0 -113
  52. package/src/index.ts +0 -48
  53. package/src/types.ts +0 -17
  54. package/src/useApi.ts +0 -68
  55. package/src/vite-env.d.ts +0 -1
  56. package/test/api.test.ts +0 -84
  57. package/test/auth.test.ts +0 -74
  58. package/test/forage.test.ts +0 -31
  59. package/test/helpers.test.ts +0 -231
  60. package/test/navigation.test.ts +0 -99
  61. package/test/stores-last-update.test.ts +0 -138
  62. package/test/stores-session.test.ts +0 -118
  63. package/test/track.test.ts +0 -29
  64. package/test/urls.test.ts +0 -42
  65. package/test/utils.ts +0 -15
  66. package/tsconfig.json +0 -28
  67. package/tsconfig.node.json +0 -11
  68. package/vite.config.ts +0 -45
@@ -1,604 +0,0 @@
1
- import type { GetOptions, OnCanDoAsync, OnDoAsync, OnDoSuccessAsync, OnGetAsync, OnGetSuccessAsync } from './actions'
2
- import type { BladeVariant } from '../types'
3
- import { useCSV } from '../composables/csv'
4
- import { isLengthyArray, hasSearch } from '../composables/helpers'
5
- import { firstBy } from 'thenby'
6
- import { computed, ref, onMounted, toValue, shallowRef, MaybeRefOrGetter, watch } from 'vue'
7
- import { useRoute, useRouter } from 'vue-router'
8
- import { useArrayDifference, useArrayUnique, watchArray, watchDebounced } from '@vueuse/core'
9
- import { useActions } from '../composables/actions'
10
- import { StorageMode, StoreMode, useStoreDefinition } from '../composables/stores'
11
-
12
- export interface RefreshOptions {
13
- deepRefresh?: boolean,
14
- resetSearch?: boolean
15
- }
16
-
17
- export interface TableColumn {
18
- align?: 'start' | 'end' | 'center'
19
- bool?: number
20
- csv?: boolean,
21
- csvArray?: boolean,
22
- csvFilter?: string,
23
- csvText?: string,
24
- hide?: boolean
25
- itemText?: string
26
- level?: number
27
- maxWidth?: string
28
- minWidth?: string
29
- nav?: string
30
- prefix?: string
31
- searchable?: boolean
32
- single?: boolean
33
- sublevel?: number,
34
- suffix?: string
35
- textFilter?: string
36
- textFunction?: Function
37
- title?: string
38
- value?: string
39
- }
40
-
41
- export interface CustomFilterParam {
42
- name: string,
43
- filterFunction: () => string,
44
- onFilterResult: Function | null
45
- }
46
-
47
- export interface ListEvents {
48
- (e: 'change', item: any): void
49
- (e: 'deleted', item: any): void
50
- (e: 'input', item: any): void
51
- (e: 'select', item: any): void
52
- (e: 'mouse-over-item', item: any): void
53
- }
54
-
55
- export interface ListProps {
56
- additionalUrl?: string
57
- bladeName?: string
58
- canAdd?: boolean
59
- canDelete?: boolean
60
- canEdit?: boolean
61
- canRestore?: boolean
62
- canShowInactive?: boolean
63
- canSelect?: boolean
64
- canUnselect?: boolean
65
- confirmOnDelete?: boolean
66
- customFilters?: CustomFilterParam[]
67
- defaultFilters?: string[]
68
- dividers?: boolean
69
- eager?: boolean
70
- errorMsg?: string
71
- headers?: TableColumn[]
72
- hideActions?: boolean
73
- hideColumns?: boolean
74
- hideFilters?: boolean
75
- hideRefresh?: boolean
76
- hideSubtoolbarSettings?: boolean
77
- includeDetails?: boolean
78
- isSingle?: boolean
79
- itemBladeName?: string
80
- itemID?: string
81
- items?: any[]
82
- itemsPerPage?: number
83
- itemText?: string
84
- // itemValue?: string
85
- lines?: 'one' | 'two' | 'three'
86
- loadingMsg?: string
87
- localOnly?: boolean //if local only or if nav is null, then will seek to only remove from items and asyncItems
88
- nav?: string
89
- /**for defining if deletable (show button or not) */
90
- onCanDelete?: (item: any) => boolean
91
- /**when trying to delete in api */
92
- onCanDeleteAsync?: OnCanDoAsync
93
- onGetSaveAsync?: OnDoSuccessAsync
94
- onCanRestore?: (item: any) => boolean
95
- onCanRestoreAsync?: OnCanDoAsync
96
- onCanSaveAsync?: OnCanDoAsync
97
- onCanSelectItem?: (item: any) => boolean
98
- onDeleteAsync?: OnDoAsync
99
- onDeleteSuccessAsync?: OnDoAsync
100
- onFilter?: Function
101
- onGetAsync?: OnGetAsync
102
- onGetSuccessAsync?: OnGetSuccessAsync
103
- onRestoreAsync?: OnDoSuccessAsync
104
- onRestoreSuccessAsync?: OnDoSuccessAsync
105
- onSaveAsync?: OnDoSuccessAsync
106
- onSaveSuccessAsync?: OnDoSuccessAsync
107
- onSelectItem?: (item: any) => void
108
- params?: any
109
- proxyID?: string
110
- proxyQueryKey?: string
111
- refreshToggle?: boolean
112
- searchProps?: string[]
113
- searchQueryKey?: string
114
- selectProps?: string[]
115
- sortBy?: string
116
- sortOrder?: 'Ascending' | 'Descending'
117
- showListOnly?: boolean
118
- showTableOnly?: boolean
119
- showSearch?: boolean
120
- startShowingInactive?: boolean
121
- storeMode?: StoreMode
122
- storageMode?: StorageMode
123
- useInterEvents?: boolean
124
- useServerPagination?: boolean
125
- useBladeSrc?: boolean
126
- useRouteSrc?: boolean
127
- variant?: BladeVariant
128
- }
129
-
130
- export interface GroupedHeaderOption {
131
- position: number,
132
- values: TableColumn[]
133
- }
134
-
135
- export interface UseListOptions {
136
- storeMode?: StoreMode
137
- storageMode?: StorageMode
138
- useBladeSrc?: boolean
139
- useRouteSrc?: boolean
140
- }
141
-
142
- /**
143
- * PROPS first
144
- * ROUTE second
145
- * BLADE third
146
- * @param props
147
- * @param emit
148
- * @param options
149
- */
150
- export function useList(props: ListProps, emit?: ListEvents, options?: UseListOptions) {
151
- // const useBladeSrc = props.useBladeSrc ?? options?.useBladeSrc ?? true
152
- // const useRouteSrc = props.useRouteSrc ?? options?.useRouteSrc ?? true
153
- const storeMode = props.storeMode ?? options?.storeMode ?? 'session'
154
- const storageMode = props.storageMode ?? options?.storageMode ?? 'local-cache'
155
- // const bladeName = props.bladeName ?? props.nav
156
- const nav = props.nav ?? props.nav ?? 'basic'
157
- const csv = useCSV()
158
-
159
- const router = useRouter()
160
- const route = useRoute()
161
-
162
- //server pagination
163
- const currentPage = ref<number>(1)
164
-
165
- //server filtering
166
- const customFilters = toValue(props.customFilters) ?? []
167
- const serverFilters = ref<string[]>([])
168
- let latestFilters = ref(new Array<number>())
169
- const allFilters = computed(() => {
170
- return useArrayUnique([
171
- ...customFilters.filter(x => x.name != null).map(x => x.name),
172
- ...(props.defaultFilters ?? []),
173
- ...serverFilters.value
174
- ]).value
175
- })
176
- const selectedFilters = ref((props.defaultFilters ?? []).map(x => allFilters.value.indexOf(x)))
177
- const filtersChanged = computed(() => {
178
- return useArrayDifference(latestFilters, selectedFilters).value.length > 0 ||
179
- useArrayDifference(selectedFilters, latestFilters).value.length > 0
180
- })
181
-
182
- //server headers
183
- const qKey = toValue(props.proxyQueryKey)
184
- const proxyID = computed(() => { return toValue(props.proxyID) ?? (qKey != null ? route.query?.[qKey] : undefined) as string | undefined; })
185
-
186
- //server query
187
- const sKey = toValue(props.searchQueryKey)
188
- const searchString = ref<string | undefined>(sKey != null ? route.query?.[sKey] as string | undefined : undefined)
189
- // const selectProps = toValue(props.selectProps) ?? []
190
- const showInactive = ref(toValue(props.startShowingInactive) ?? false)
191
- const sortOrder = ref(toValue(props.sortOrder))
192
-
193
- //list
194
- const searchableProps = computed(() => {
195
- return [
196
- ...(props.searchProps ?? []),
197
- ...(props.headers ?? []).filter(x => x.searchable != null && x.value != null).map(x => x.value ?? '')
198
- ]
199
- })
200
- const asyncItems = ref<any[]>([])
201
- const filteredItems = shallowRef<any[]>([])
202
- // let filtersLoaded = false
203
-
204
- const headerOptions = ref<TableColumn[]>([])
205
-
206
- let lastSelectedItem: any
207
-
208
- const { actionErrorMsg, actionLoadingMsg, deleteItem, doAction, getItem, getAllItems, restoreItem } = useActions({
209
- nav: props.nav ?? props.bladeName ?? props.itemBladeName,
210
- proxyID: proxyID.value,
211
- store: useStoreDefinition({
212
- storeMode: storeMode,
213
- storageMode: storageMode,
214
- storeName: nav
215
- })
216
- })
217
-
218
- const errorMsg = computed(() => props.errorMsg ?? actionErrorMsg.value)
219
- const loadingMsg = computed(() => props.loadingMsg ?? actionLoadingMsg.value)
220
- const isLoading = computed(() => loadingMsg.value != null)
221
- const showError = shallowRef(false)
222
- const showSearch = shallowRef(props.showSearch ?? false)
223
- const totalPages = ref(0)
224
-
225
- const isDeletable = computed(() => (item: any) => {
226
- if (props.onCanDelete != null)
227
- return props.onCanDelete(item)
228
-
229
- if (item != null && item.isInactive === true)
230
- return false
231
-
232
- return true
233
- })
234
-
235
- const isRestorable = computed(() => (item: any) => {
236
- if (!showInactive.value)
237
- return false
238
-
239
- if (props.onCanRestore != null)
240
- return props.onCanRestore(item)
241
-
242
- if (item?.isInactive === true)
243
- return true
244
-
245
- return false
246
- })
247
-
248
- const tableHeaders = computed(() => { return headerOptions.value.filter(x => !x.hide) })
249
- const subtitleOptions = computed(() => {
250
- let returnList: GroupedHeaderOption[] = new Array<GroupedHeaderOption>()
251
- const scanList = tableHeaders.value.filter(x => x.sublevel != undefined)
252
- scanList.forEach(s => {
253
- let e = returnList.find(x => x.position == s.sublevel)
254
- if (e == null) {
255
- returnList.push({ position: s.sublevel ?? 0, values: [s] })
256
- }
257
- else {
258
- e.values.push(s)
259
- }
260
- })
261
- return returnList.sort(firstBy(x => x.position))
262
- })
263
- const titleOptions = computed(() => {
264
- let returnList: GroupedHeaderOption[] = new Array<GroupedHeaderOption>()
265
- const scanList = tableHeaders.value.filter(x => x.level != undefined) // && !Number.isNaN(x.title))
266
- scanList.forEach(s => {
267
- let e = returnList.find(x => x.position == s.level)
268
- if (e == null) {
269
- returnList.push({ position: s.level ?? 0, values: [s] })
270
- }
271
- else {
272
- e.values.push(s)
273
- }
274
- })
275
- return returnList.sort(firstBy(x => x.position))
276
- })
277
- const displayHeaders = computed(() => {
278
- return []
279
- // return tableHeaders.value.filter(x => (x.nav != null && x.itemText != null) || x.textFilter != null || x.display != null || x.bool != null)
280
- })
281
-
282
- function add(variant: BladeVariant) {
283
- if (props.itemBladeName != null) {
284
- // if (allProps.useInterEvents) {
285
- // eventBus?.sendSelect(allProps.addBladeName ?? '', 'new')
286
- // }
287
- // else {
288
- let bladeData = {
289
- bladeName: props.itemBladeName,
290
- id: 'new',
291
- nav
292
- }
293
-
294
- if (variant == 'page') {
295
- router.push({
296
- name: bladeData.bladeName,
297
- params: { id: 'new' }
298
- })
299
- }
300
- else if (variant == 'freestyle' || variant == 'blade') {
301
-
302
- }
303
- // }
304
- }
305
- }
306
-
307
- function mDeleteItem(dItem: MaybeRefOrGetter<any>) {
308
- const {
309
- additionalUrl,
310
- onDeleteAsync,
311
- } = { ...props }
312
- const item = toValue(dItem)
313
- const id = item.id
314
-
315
- const removeLocally = (id: string) => {
316
- if (id != null) {
317
- let asyncInd = asyncItems.value.findIndex(x => x.id == id)
318
- if (asyncInd > -1) {
319
- asyncItems.value.splice(asyncInd, 1)
320
- }
321
-
322
- if (props.items != null) {
323
- let ind = props.items.findIndex(x => x.id == id)
324
-
325
- if (ind > -1)
326
- props.items.splice(ind, 1)
327
- }
328
- }
329
- else {
330
- let asyncInd = asyncItems.value.findIndex(x => x === item) //.findIndex(x => x.id == id)
331
- if (asyncInd == -1)
332
- asyncInd = asyncItems.value.findIndex(x => x == item)
333
-
334
- if (asyncInd > -1)
335
- asyncItems.value.splice(asyncInd, 1)
336
-
337
- if (props.items != null) {
338
- let ind = props.items.findIndex(x => x === item) //.findIndex(y => y.id == id)
339
-
340
- if (ind == -1)
341
- ind = props.items.findIndex(x => x == item)
342
-
343
- if (ind > -1)
344
- props.items.splice(ind, 1)
345
- }
346
- }
347
-
348
- // eventBus?.sendDelete(nav ?? '', id)
349
- }
350
-
351
- if (props.localOnly == true || nav == null) {
352
- removeLocally(id)
353
- }
354
- else {
355
- deleteItem({
356
- additionalUrl,
357
- data: item,
358
- nav,
359
- onCanDeleteAsync: props.onCanDeleteAsync,
360
- onDeleteAsync,
361
- onDeleteSuccessAsync: async () => {
362
- removeLocally(id)
363
-
364
- if (props.onDeleteSuccessAsync != null)
365
- return props.onDeleteSuccessAsync(item)
366
-
367
- return Promise.resolve(undefined)
368
- },
369
- proxyID: proxyID.value,
370
- // ...(useBladeSrc ? bladeData.value : {}),
371
- requireConfirmation: true
372
- })
373
- }
374
-
375
- if (emit)
376
- emit('deleted', dItem)
377
- }
378
-
379
- function mRestoreItem(dItem: MaybeRefOrGetter<any>) {
380
- restoreItem({
381
- data: toValue(dItem),
382
- additionalUrl: props.additionalUrl,
383
- nav,
384
- onCanRestoreAsync: props.onCanRestoreAsync,
385
- onRestoreAsync: props.onRestoreAsync,
386
- onRestoreSuccessAsync: props.onRestoreSuccessAsync,
387
- proxyID: proxyID.value,
388
- // ...(useBladeSrc ? bladeData.value : {})
389
- })
390
- }
391
-
392
- function exportToCSV() {
393
- doAction(() => {
394
- csv.exportToCSV(tableHeaders.value, filteredItems.value)
395
- }, { loadingMsg: 'Exporting to CSV' })
396
- }
397
-
398
- function refreshFilteredList() {
399
- let l = toValue(asyncItems)
400
- l = props.onFilter ? props.onFilter(l) : l
401
-
402
- if (searchString.value != null && searchString.value.length > 0) {
403
- let sProps = [...searchableProps.value]
404
- if (props.itemText)
405
- sProps.push(props.itemText)
406
-
407
- if (isLengthyArray(sProps))
408
- l = l.filter(x => hasSearch(x, searchString.value, sProps))
409
- }
410
-
411
- filteredItems.value = l
412
- }
413
-
414
- function refreshTableHeaders() {
415
- if (props.headers != null) {
416
- headerOptions.value = [...props.headers]
417
-
418
- if (!props.hideActions)
419
- headerOptions.value.push({ title: 'Actions', value: 'itemActions', align: 'end' })
420
- }
421
- }
422
-
423
- async function refresh(options?: RefreshOptions) {
424
- showError.value = false
425
-
426
- if (options?.resetSearch === true) {
427
- showSearch.value = props.showSearch ?? false
428
- searchString.value = undefined
429
- }
430
-
431
- if (props.items != null) {
432
- asyncItems.value = props.onGetSuccessAsync != null ? await props.onGetSuccessAsync(props.items) : props.items
433
- return
434
- }
435
-
436
- const getOptions: GetOptions = {
437
- additionalUrl: props.additionalUrl,
438
- id: props.itemID,
439
- nav: props.nav,
440
- // params: params.getParamOptions(),
441
- proxyID: proxyID.value,
442
- // ...(useBladeSrc ? bladeData.value : {}),
443
- // ...(props.useInterEvents ? eventData : {}),
444
- refresh: options?.deepRefresh ?? false,
445
- onGetAsync: props.onGetAsync,
446
- onGetSuccessAsync: props.onGetSuccessAsync,
447
- }
448
-
449
- if (props.isSingle === true) {
450
- if (getOptions.id === 'new')
451
- asyncItems.value = []
452
- else
453
- console.log('getting')
454
- asyncItems.value = await getItem(getOptions)
455
- console.log('gottne')
456
- }
457
- else {
458
- let r = await getAllItems({
459
- ...getOptions,
460
- onGetSuccessAsync: async (gRes, opt) => {
461
- let dataRes = gRes.data
462
- // if (!filtersLoaded) {
463
- serverFilters.value = gRes.filters ?? []
464
- // filtersLoaded = true
465
- // }
466
-
467
- if (props.useServerPagination === true &&
468
- props.itemsPerPage &&
469
- gRes.count) {
470
- totalPages.value = Math.ceil(gRes.count / props.itemsPerPage)
471
- }
472
-
473
- if (props.onGetSuccessAsync != null)
474
- dataRes = await props.onGetSuccessAsync(dataRes, opt)
475
-
476
- return dataRes
477
- }
478
- })
479
-
480
- asyncItems.value = r
481
- }
482
-
483
- refreshFilteredList()
484
- }
485
-
486
- function selectItem(rItem: any, variant: BladeVariant) {
487
- const item = toValue(rItem)
488
- if (item != null && item === lastSelectedItem && props.canUnselect) {
489
- lastSelectedItem = null;
490
- }
491
- else {
492
- lastSelectedItem = item
493
- }
494
-
495
- if (emit != null) {
496
- emit('select', lastSelectedItem)
497
- }
498
-
499
- if (!props.canSelect || (props.onCanSelectItem != null && !props.onCanSelectItem(item)))
500
- return;
501
-
502
- if (props.onSelectItem != null) {
503
- props.onSelectItem(lastSelectedItem)
504
- }
505
- // else if (lastSelectedItem == null) {
506
- // //close blade
507
- // sendClose({ bladeName: allProps.addBladeName })
508
- // }
509
- else {
510
- // if (props.useInterEvents) {
511
- // console.log('is selecting')
512
- // eventBus?.sendSelect(allProps.addBladeName ?? '', lastSelectedItem?.id)
513
- // }
514
- // else {
515
- let bladeData = {
516
- bladeName: props.itemBladeName,
517
- id: item.id,
518
- nav: props.nav
519
- }
520
-
521
- if (variant == 'page') {
522
- router.push({
523
- name: bladeData.bladeName,
524
- params: { id: bladeData.id }
525
- })
526
- }
527
- else if (variant == 'freestyle' || variant == 'blade') {
528
- // sendUpdate(bladeData)
529
- }
530
- // }
531
- }
532
- }
533
-
534
- function toggleSearch() {
535
- if (showSearch.value) {
536
- showSearch.value = props.showSearch ?? false
537
- searchString.value = undefined
538
- refresh()
539
- }
540
- else {
541
- showSearch.value = props.showSearch ?? true
542
- }
543
- }
544
-
545
- refreshTableHeaders()
546
-
547
- watchDebounced([searchString], () => {
548
- refreshFilteredList()
549
- }, { debounce: 500, maxWait: 500 })
550
-
551
- watch([showInactive, currentPage], async () => { //bladeData
552
- await refresh()
553
- })
554
-
555
- watch([errorMsg, () => props.errorMsg], ([vOne, vTwo]) => {
556
- showError.value = vOne != null || vTwo != null
557
- })
558
-
559
- watch(() => props.refreshToggle, () => {
560
- refresh({ deepRefresh: true })
561
- })
562
-
563
- watchArray([asyncItems, () => props.items], () => {
564
- refreshFilteredList()
565
- })
566
-
567
- onMounted(async () => {
568
- if (props.eager == true)
569
- await refresh({ deepRefresh: route.params?.refresh == 'true' })
570
- })
571
-
572
- return {
573
- add,
574
- asyncItems,
575
- currentPage,
576
- deleteItem: mDeleteItem,
577
- displayHeaders,
578
- errorMsg,
579
- exportToCSV,
580
- filteredItems,
581
- filters: allFilters,
582
- filtersChanged,
583
- headerOptions,
584
- isDeletable,
585
- isLoading,
586
- isRestorable,
587
- loadingMsg,
588
- refresh,
589
- restoreItem: mRestoreItem,
590
- searchString,
591
- selectedFilters,
592
- selectItem,
593
- serverFilters,
594
- showError,
595
- showInactive,
596
- showSearch,
597
- sortOrder,
598
- subtitleOptions,
599
- tableHeaders,
600
- titleOptions,
601
- toggleSearch,
602
- totalPages
603
- }
604
- }