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